From 93e7f76166c392f558d25f71424a545a2a490ff8 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 20 May 2026 16:28:06 +0800 Subject: [PATCH 1/5] Add Feishu channel extension support --- README.md | 15 ++ config.yaml.full | 5 + docs/docs/configuration.md | 3 + docs/docs/tools/feishu-channel.md | 53 +++++ docs/examples/channel/feishu_bot.py | 32 +++ docs/mkdocs.yml | 1 + pyproject.toml | 3 +- tests/test_feishu_channel_extension.py | 118 ++++++++++ veadk/extensions/__init__.py | 17 ++ veadk/extensions/feishu_channel.py | 285 +++++++++++++++++++++++++ 10 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 docs/docs/tools/feishu-channel.md create mode 100644 docs/examples/channel/feishu_bot.py create mode 100644 tests/test_feishu_channel_extension.py create mode 100644 veadk/extensions/__init__.py create mode 100644 veadk/extensions/feishu_channel.py diff --git a/README.md b/README.md index 4facc50e..33ffefd2 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,21 @@ res = asyncio.run(agent.run("hello!")) print(res) ``` +## Feishu bot channel + +VeADK now provides `veadk.extensions.FeishuChannelExtension` for bridging a Feishu bot with a `Runner`. It maps `union_id` to `user_id`, and `thread_id` / `chat_id` to `session_id`, so VeADK memory and tracing can work directly in Feishu conversations. + +```python +from veadk import Agent, Runner +from veadk.extensions import FeishuChannelExtension + +agent = Agent() +runner = Runner(agent=agent, app_name="feishu_demo") +channel = FeishuChannelExtension(runner=runner) +``` + +Configure credentials with `TOOL_FEISHU_CHANNEL_APP_ID` and `TOOL_FEISHU_CHANNEL_APP_SECRET`, or in `config.yaml` under `tool.feishu_channel`. + ## Command line tools VeADK provides several useful command line tools for faster deployment and optimization, such as: diff --git a/config.yaml.full b/config.yaml.full index e29ed159..af7650ee 100644 --- a/config.yaml.full +++ b/config.yaml.full @@ -56,6 +56,11 @@ tool: endpoint: # `app_id` api_key: # `app_secret` token: # `user_token` + # [optional] for Feishu bot channel extension based on lark_oapi.channel + feishu_channel: + app_id: + app_secret: + transport: ws # `ws` | `webhook` # [optional] for Volcengine Lake AI Service https://www.volcengine.com/product/las mobile_use: tool_id: #https://console.volcengine.com/ACEP diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index efc03851..49f3ffea 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -60,6 +60,9 @@ volcengine: | Lark | `TOOL_LARK_ENDPOINT` | Lark 应用 ID(app_id) | | | `TOOL_LARK_API_KEY` | Lark 应用密钥(app_secret) | | | `TOOL_LARK_TOKEN` | Lark 用户 token | +| Feishu Channel | `TOOL_FEISHU_CHANNEL_APP_ID` | 飞书机器人应用 ID(app_id) | +| | `TOOL_FEISHU_CHANNEL_APP_SECRET` | 飞书机器人应用密钥(app_secret) | +| | `TOOL_FEISHU_CHANNEL_TRANSPORT` | Channel 传输模式,默认 `ws` | | LAS | `TOOL_LAS_URL` | LAS SSE 服务地址(含 token) | | | `TOOL_LAS_DATASET_ID` | LAS 数据集 ID | | VOD | `TOOL_VOD_GROUPS` | 视频编辑能力组 | diff --git a/docs/docs/tools/feishu-channel.md b/docs/docs/tools/feishu-channel.md new file mode 100644 index 00000000..f395d8ec --- /dev/null +++ b/docs/docs/tools/feishu-channel.md @@ -0,0 +1,53 @@ +# 飞书 Channel 扩展 + +`veadk.extensions.FeishuChannelExtension` 用于把飞书机器人的入站消息桥接到 VeADK `Runner`。 + +它默认基于 `lark_oapi.channel.FeishuChannel` 的 `message` 事件工作,并按下面的规则映射会话身份: + +- `sender.union_id -> Runner.user_id` +- `conversation.thread_id -> Runner.session_id` +- 如果线程 ID 不存在,则回退到 `chat_id -> Runner.session_id` + +这样做的好处是,VeADK 现有的短期记忆、长期记忆、Tracing 和多租户隔离能力可以直接复用。 + +## 安装 + +```bash +pip install veadk-python[extensions] +``` + +如果你只想安装这个能力,也可以单独安装: + +```bash +pip install lark-oapi +``` + +## 配置 + +环境变量: + +- `TOOL_FEISHU_CHANNEL_APP_ID` +- `TOOL_FEISHU_CHANNEL_APP_SECRET` +- `TOOL_FEISHU_CHANNEL_TRANSPORT`,默认 `ws` + +或在 `config.yaml` 中配置: + +```yaml title="config.yaml" +tool: + feishu_channel: + app_id: cli_xxx + app_secret: xxx + transport: ws +``` + +## 最小示例 + +```python +--8<-- "examples/channel/feishu_bot.py" +``` + +## 说明 + +- 默认使用飞书 `Channel` 的 WebSocket 模式,因此只要机器人已订阅消息事件,就可以直接启动连接。 +- 默认回复会使用 `reply_to=原消息 message_id`,让 VeADK 输出继续挂在当前飞书消息线程下。 +- 你可以通过 `session_id_factory` 和 `user_id_factory` 覆盖默认映射逻辑。 diff --git a/docs/examples/channel/feishu_bot.py b/docs/examples/channel/feishu_bot.py new file mode 100644 index 00000000..ff5adcc0 --- /dev/null +++ b/docs/examples/channel/feishu_bot.py @@ -0,0 +1,32 @@ +import asyncio + +from veadk import Agent, Runner +from veadk.extensions import FeishuChannelExtension +from veadk.memory.short_term_memory import ShortTermMemory + +agent = Agent( + name="feishu_agent", + instruction="你是一个通过飞书机器人与用户沟通的助手。", +) + +runner = Runner( + agent=agent, + app_name="veadk_feishu_demo", + user_id="veadk_feishu_default_user", + short_term_memory=ShortTermMemory(), +) + +channel = FeishuChannelExtension( + runner=runner, + channel_kwargs={ + "transport": "ws", + }, +) + + +async def main(): + await channel.connect() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index eaf6d653..fe600629 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -55,6 +55,7 @@ nav: # - 最佳实践(记忆相关): memory/best-practice-memory.md - 连接能力源——工具: - 内置工具: tools/builtin.md + - 飞书 Channel 扩展: tools/feishu-channel.md - 自定义工具: tools/function.md - 护栏工具: tools/guardrail.md - 连接数据源——知识库: diff --git a/pyproject.toml b/pyproject.toml index 46ad2a87..31a7fa98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ extensions = [ "llama-index-vector-stores-redis>=0.6.1", # For Redis database "llama-index-vector-stores-opensearch==0.6.1", # For Opensearch database "opensearch-py==2.8.0", + "lark-oapi", ] database = [ "redis>=5.0", # For Redis database @@ -97,4 +98,4 @@ include-package-data = true exclude = [ "veadk/integrations/ve_faas/template/*", "veadk/integrations/ve_faas/web_template/*" -] \ No newline at end of file +] diff --git a/tests/test_feishu_channel_extension.py b/tests/test_feishu_channel_extension.py new file mode 100644 index 00000000..b469bc9f --- /dev/null +++ b/tests/test_feishu_channel_extension.py @@ -0,0 +1,118 @@ +from types import SimpleNamespace + +import pytest + +from veadk.extensions.feishu_channel import FeishuChannelExtension + + +class FakeChannel: + def __init__(self): + self.handlers = {} + self.sent_messages = [] + + def on(self, event_name, handler): + self.handlers[event_name] = handler + + async def send(self, chat_id, body, options=None): + self.sent_messages.append((chat_id, body, options)) + + +class FakeRunner: + def __init__(self): + self.calls = [] + + async def run(self, messages, user_id="", session_id="", **kwargs): + self.calls.append( + { + "messages": messages, + "user_id": user_id, + "session_id": session_id, + } + ) + return f"echo:{messages}" + + +def build_message(**overrides): + message = SimpleNamespace( + id="om_001", + message_id="om_001", + chat_id="oc_chat", + chat_type="p2p", + thread_id="", + reply_to_message_id="", + content_text="你好", + sender_id="ou_sender", + sender=SimpleNamespace( + union_id="on_union", + open_id="ou_sender", + user_id="u_sender", + ), + conversation=SimpleNamespace( + chat_id="oc_chat", + chat_type="p2p", + thread_id="", + ), + reply=SimpleNamespace(message_id=""), + ) + for key, value in overrides.items(): + setattr(message, key, value) + return message + + +@pytest.mark.asyncio +async def test_extension_uses_union_id_and_thread_id(): + runner = FakeRunner() + channel = FakeChannel() + extension = FeishuChannelExtension(runner=runner, channel=channel) + + message = build_message( + thread_id="thread_1", + conversation=SimpleNamespace( + chat_id="oc_chat", + chat_type="group", + thread_id="thread_1", + ), + ) + + await extension._on_message(message) + + assert runner.calls == [ + { + "messages": "你好", + "user_id": "on_union", + "session_id": "thread_1", + } + ] + assert channel.sent_messages == [ + ("oc_chat", {"text": "echo:你好"}, {"reply_to": "om_001"}) + ] + + +@pytest.mark.asyncio +async def test_extension_falls_back_to_chat_id_when_thread_missing(): + runner = FakeRunner() + channel = FakeChannel() + extension = FeishuChannelExtension(runner=runner, channel=channel) + + message = build_message( + sender=SimpleNamespace(union_id="", open_id="ou_fallback", user_id="u_sender") + ) + + await extension._on_message(message) + + assert runner.calls[0]["user_id"] == "ou_fallback" + assert runner.calls[0]["session_id"] == "oc_chat" + + +@pytest.mark.asyncio +async def test_extension_ignores_empty_message_by_default(): + runner = FakeRunner() + channel = FakeChannel() + extension = FeishuChannelExtension(runner=runner, channel=channel) + + message = build_message(content_text=" ") + + await extension._on_message(message) + + assert runner.calls == [] + assert channel.sent_messages == [] diff --git a/veadk/extensions/__init__.py b/veadk/extensions/__init__.py new file mode 100644 index 00000000..20ff8d38 --- /dev/null +++ b/veadk/extensions/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from veadk.extensions.feishu_channel import FeishuChannelExtension + +__all__ = ["FeishuChannelExtension"] diff --git a/veadk/extensions/feishu_channel.py b/veadk/extensions/feishu_channel.py new file mode 100644 index 00000000..fea4632e --- /dev/null +++ b/veadk/extensions/feishu_channel.py @@ -0,0 +1,285 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import os +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Awaitable, Callable + +from veadk.utils.logger import get_logger + +if TYPE_CHECKING: + from veadk.runner import Runner + +logger = get_logger(__name__) + +MessageHandler = Callable[["FeishuMessageContext"], Awaitable[str | None] | str | None] +SessionIdFactory = Callable[[Any], str] +UserIdFactory = Callable[[Any], str] + + +def _coalesce(*values: Any) -> str: + for value in values: + if value: + return str(value) + return "" + + +def _read_attr(obj: Any, *path: str) -> Any: + current = obj + for key in path: + if current is None: + return None + current = getattr(current, key, None) + return current + + +@dataclass(slots=True) +class FeishuMessageContext: + message_id: str + chat_id: str + chat_type: str + thread_id: str + reply_to_message_id: str + user_id: str + session_id: str + union_id: str + open_id: str + raw_message: Any + text: str + + +class FeishuChannelExtension: + """Bridge a Feishu bot channel with a VeADK runner. + + The extension subscribes to normalized ``message`` events from + ``lark_oapi.channel.FeishuChannel`` and forwards the incoming text to a VeADK + ``Runner``. It maps Feishu sender identity to VeADK ``user_id`` and Feishu + conversation/thread identity to VeADK ``session_id`` so existing short-term + memory, long-term memory and tracing continue to work without changes. + """ + + def __init__( + self, + runner: "Runner", + *, + app_id: str | None = None, + app_secret: str | None = None, + channel: Any | None = None, + session_id_factory: SessionIdFactory | None = None, + user_id_factory: UserIdFactory | None = None, + message_handler: MessageHandler | None = None, + response_formatter: Callable[[str], dict[str, str]] | None = None, + reply_in_thread: bool = True, + ignore_empty_messages: bool = True, + channel_kwargs: dict[str, Any] | None = None, + ) -> None: + self.runner = runner + self.session_id_factory = session_id_factory or self.default_session_id_factory + self.user_id_factory = user_id_factory or self.default_user_id_factory + self.message_handler = message_handler + self.response_formatter = response_formatter or self.default_response_formatter + self.reply_in_thread = reply_in_thread + self.ignore_empty_messages = ignore_empty_messages + + if channel is not None: + self.channel = channel + else: + self.channel = self._build_channel( + app_id=app_id, + app_secret=app_secret, + channel_kwargs=channel_kwargs, + ) + + self.channel.on("message", self._on_message) + + @staticmethod + def default_user_id_factory(message: Any) -> str: + sender = _read_attr(message, "sender") + user_id = _coalesce( + getattr(sender, "union_id", None), + getattr(sender, "open_id", None), + getattr(sender, "user_id", None), + getattr(message, "sender_id", None), + ) + if user_id: + return user_id + raise ValueError("Cannot resolve Feishu sender identity into a VeADK user_id.") + + @staticmethod + def default_session_id_factory(message: Any) -> str: + thread_id = _coalesce( + _read_attr(message, "conversation", "thread_id"), + getattr(message, "thread_id", None), + getattr(message, "reply_to_message_id", None), + ) + chat_id = _coalesce( + getattr(message, "chat_id", None), + _read_attr(message, "conversation", "chat_id"), + ) + return thread_id or chat_id or getattr(message, "message_id", "") + + @staticmethod + def default_response_formatter(text: str) -> dict[str, str]: + return {"text": text} + + async def connect(self) -> Any: + return await self._maybe_await(self.channel.connect()) + + async def disconnect(self) -> Any: + disconnect = getattr(self.channel, "disconnect", None) + if disconnect is None: + return None + result = disconnect() + if inspect.isawaitable(result): + return await result + return result + + async def handle_webhook_request( + self, headers: dict[str, str], body: bytes | str + ) -> Any: + handler = getattr(self.channel, "handle_webhook_request", None) + if handler is None: + raise AttributeError("Current channel does not support webhook requests.") + result = handler(headers, body) + if inspect.isawaitable(result): + return await result + return result + + async def _on_message(self, message: Any) -> None: + text = str(getattr(message, "content_text", "") or "").strip() + if self.ignore_empty_messages and not text: + logger.debug( + f"Ignore empty Feishu message: {getattr(message, 'message_id', '')}" + ) + return + + context = self.build_message_context(message=message, text=text) + + if self.message_handler is not None: + response_text = await self._maybe_await(self.message_handler(context)) + else: + response_text = await self.runner.run( + messages=context.text, + user_id=context.user_id, + session_id=context.session_id, + ) + + if not response_text: + return + + send_options = {} + if self.reply_in_thread and context.message_id: + send_options["reply_to"] = context.message_id + + await self._maybe_await( + self.channel.send( + context.chat_id, + self.response_formatter(str(response_text)), + send_options, + ) + ) + + def build_message_context( + self, message: Any, text: str | None = None + ) -> FeishuMessageContext: + user_id = self.user_id_factory(message) + session_id = self.session_id_factory(message) + message_id = _coalesce( + getattr(message, "message_id", None), + getattr(message, "id", None), + ) + chat_id = _coalesce( + getattr(message, "chat_id", None), + _read_attr(message, "conversation", "chat_id"), + ) + chat_type = _coalesce( + getattr(message, "chat_type", None), + _read_attr(message, "conversation", "chat_type"), + ) + thread_id = _coalesce( + getattr(message, "thread_id", None), + _read_attr(message, "conversation", "thread_id"), + ) + reply_to_message_id = _coalesce( + getattr(message, "reply_to_message_id", None), + _read_attr(message, "reply", "message_id"), + ) + union_id = _coalesce(_read_attr(message, "sender", "union_id")) + open_id = _coalesce( + _read_attr(message, "sender", "open_id"), + getattr(message, "sender_id", None), + ) + + return FeishuMessageContext( + message_id=message_id, + chat_id=chat_id, + chat_type=chat_type, + thread_id=thread_id, + reply_to_message_id=reply_to_message_id, + user_id=user_id, + session_id=session_id, + union_id=union_id, + open_id=open_id, + raw_message=message, + text=text + if text is not None + else str(getattr(message, "content_text", "") or ""), + ) + + def _build_channel( + self, + *, + app_id: str | None, + app_secret: str | None, + channel_kwargs: dict[str, Any] | None, + ) -> Any: + try: + from lark_oapi.channel import FeishuChannel + except ImportError as exc: + raise ImportError( + "Feishu channel extension requires `lark-oapi`. " + "Install `veadk-python[extensions]` or `pip install lark-oapi`." + ) from exc + + resolved_app_id = app_id or os.getenv("TOOL_FEISHU_CHANNEL_APP_ID") or os.getenv( + "TOOL_LARK_ENDPOINT" + ) + resolved_app_secret = app_secret or os.getenv( + "TOOL_FEISHU_CHANNEL_APP_SECRET" + ) or os.getenv("TOOL_LARK_API_KEY") + + if not resolved_app_id or not resolved_app_secret: + raise ValueError( + "Missing Feishu app credentials. Set `app_id` / `app_secret` or configure " + "`TOOL_FEISHU_CHANNEL_APP_ID` / `TOOL_FEISHU_CHANNEL_APP_SECRET` " + "(compatible fallback: `TOOL_LARK_ENDPOINT` / `TOOL_LARK_API_KEY`)." + ) + + resolved_channel_kwargs = dict(channel_kwargs or {}) + resolved_channel_kwargs.setdefault( + "transport", os.getenv("TOOL_FEISHU_CHANNEL_TRANSPORT", "ws") + ) + + return FeishuChannel( + app_id=resolved_app_id, + app_secret=resolved_app_secret, + **resolved_channel_kwargs, + ) + + @staticmethod + async def _maybe_await(value: Any) -> Any: + if inspect.isawaitable(value): + return await value + return value From af84846d1acf3a79c977d02b4a45a4928fe2c352 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 20 May 2026 17:20:22 +0800 Subject: [PATCH 2/5] format --- tests/realtime/test_live.py | 33 +++++++++++---------- tests/test_agent.py | 41 +++++++++++++++++--------- tests/test_feishu_channel_extension.py | 14 +++++++++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/tests/realtime/test_live.py b/tests/realtime/test_live.py index 7f61d58b..30fd4750 100644 --- a/tests/realtime/test_live.py +++ b/tests/realtime/test_live.py @@ -13,7 +13,7 @@ # limitations under the License. import pytest -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from veadk.realtime.live import DoubaoAsyncSession, ProtocolEvents from veadk.realtime import protocol from google.genai import types @@ -111,21 +111,22 @@ async def test_receive(mock_session): for response_data, parse_data, expected in test_cases: mock_session._ws.recv = AsyncMock(return_value=response_data) - protocol.parse_response = MagicMock(return_value=parse_data) - async for msg in mock_session.receive(): - if response_data["event"] == ProtocolEvents.ASR_INFO: - assert msg.server_content.interrupted == expected - elif response_data["event"] == ProtocolEvents.ASR_RESPONSE: - assert msg.server_content.input_transcription.text == expected - elif response_data["event"] == ProtocolEvents.TTS_RESPONSE: - assert ( - msg.server_content.model_turn.parts[0].inline_data.data == expected - ) - elif response_data["event"] == ProtocolEvents.CHAT_RESPONSE: - assert msg.server_content.output_transcription.text == expected - elif response_data["event"] == ProtocolEvents.USAGE_RESPONSE: - assert msg.usage_metadata.tool_use_prompt_token_count == expected - break + with patch.object(protocol, "parse_response", return_value=parse_data): + async for msg in mock_session.receive(): + if response_data["event"] == ProtocolEvents.ASR_INFO: + assert msg.server_content.interrupted == expected + elif response_data["event"] == ProtocolEvents.ASR_RESPONSE: + assert msg.server_content.input_transcription.text == expected + elif response_data["event"] == ProtocolEvents.TTS_RESPONSE: + assert ( + msg.server_content.model_turn.parts[0].inline_data.data + == expected + ) + elif response_data["event"] == ProtocolEvents.CHAT_RESPONSE: + assert msg.server_content.output_transcription.text == expected + elif response_data["event"] == ProtocolEvents.USAGE_RESPONSE: + assert msg.usage_metadata.tool_use_prompt_token_count == expected + break @pytest.mark.asyncio diff --git a/tests/test_agent.py b/tests/test_agent.py index 86b55855..a24c12ce 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -13,7 +13,7 @@ # limitations under the License. import os -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch from google.adk.agents.llm_agent import LlmAgent from google.adk.models.lite_llm import LiteLlm @@ -76,19 +76,32 @@ def test_agent(): @patch.dict("os.environ", {"MODEL_AGENT_API_KEY": "mock_api_key"}) def test_agent_default_values(): - agent = Agent() - - assert agent.name == DEFAULT_AGENT_NAME - - assert agent.model_name == DEFAULT_MODEL_AGENT_NAME - assert agent.model_provider == DEFAULT_MODEL_AGENT_PROVIDER - assert agent.model_api_base == DEFAULT_MODEL_AGENT_API_BASE - - assert agent.tools == [] - assert agent.sub_agents == [] - assert agent.knowledgebase is None - assert agent.long_term_memory is None - # assert agent.tracers == [] + with ( + patch("veadk.agent.settings.model.name", new=DEFAULT_MODEL_AGENT_NAME), + patch("veadk.agent.settings.model.provider", new=DEFAULT_MODEL_AGENT_PROVIDER), + patch( + "veadk.agent.settings.model.api_base", + new=DEFAULT_MODEL_AGENT_API_BASE, + ), + patch( + "veadk.configs.model_configs.ModelConfig.api_key", + new_callable=PropertyMock, + return_value="mock_api_key", + ), + ): + agent = Agent() + + assert agent.name == DEFAULT_AGENT_NAME + + assert agent.model_name == DEFAULT_MODEL_AGENT_NAME + assert agent.model_provider == DEFAULT_MODEL_AGENT_PROVIDER + assert agent.model_api_base == DEFAULT_MODEL_AGENT_API_BASE + + assert agent.tools == [] + assert agent.sub_agents == [] + assert agent.knowledgebase is None + assert agent.long_term_memory is None + # assert agent.tracers == [] @patch.dict("os.environ", {"MODEL_AGENT_API_KEY": "mock_api_key"}) diff --git a/tests/test_feishu_channel_extension.py b/tests/test_feishu_channel_extension.py index b469bc9f..0f059ed2 100644 --- a/tests/test_feishu_channel_extension.py +++ b/tests/test_feishu_channel_extension.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from types import SimpleNamespace import pytest From bef5edbb9b90976040f884b10de7aa174858cf07 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 20 May 2026 17:51:58 +0800 Subject: [PATCH 3/5] format fix --- veadk/extensions/feishu_channel.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/veadk/extensions/feishu_channel.py b/veadk/extensions/feishu_channel.py index fea4632e..d0c7b8c9 100644 --- a/veadk/extensions/feishu_channel.py +++ b/veadk/extensions/feishu_channel.py @@ -253,12 +253,16 @@ def _build_channel( "Install `veadk-python[extensions]` or `pip install lark-oapi`." ) from exc - resolved_app_id = app_id or os.getenv("TOOL_FEISHU_CHANNEL_APP_ID") or os.getenv( - "TOOL_LARK_ENDPOINT" + resolved_app_id = ( + app_id + or os.getenv("TOOL_FEISHU_CHANNEL_APP_ID") + or os.getenv("TOOL_LARK_ENDPOINT") + ) + resolved_app_secret = ( + app_secret + or os.getenv("TOOL_FEISHU_CHANNEL_APP_SECRET") + or os.getenv("TOOL_LARK_API_KEY") ) - resolved_app_secret = app_secret or os.getenv( - "TOOL_FEISHU_CHANNEL_APP_SECRET" - ) or os.getenv("TOOL_LARK_API_KEY") if not resolved_app_id or not resolved_app_secret: raise ValueError( From 4a8f63621220ef3ded23d313a0aa42db7aa1a1b9 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 20 May 2026 17:56:12 +0800 Subject: [PATCH 4/5] fix a2a-sdk==0.3.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31a7fa98..b2b9e0a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [ ] dependencies = [ "pydantic-settings==2.10.1", # Config management - "a2a-sdk>=0.3.7", # For Google Agent2Agent protocol + "a2a-sdk==0.3.7", # For Google Agent2Agent protocol "deprecated==1.2.18", "google-adk>=1.19.0", # For basic agent architecture "litellm>=1.74.3,<=1.82.6", # For model inference From a52ebfd9d3797c1e87343916a607243dc709293c Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 20 May 2026 18:00:19 +0800 Subject: [PATCH 5/5] fix version of adk google-adk==1.19.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b2b9e0a4..5e8addfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "pydantic-settings==2.10.1", # Config management "a2a-sdk==0.3.7", # For Google Agent2Agent protocol "deprecated==1.2.18", - "google-adk>=1.19.0", # For basic agent architecture + "google-adk==1.19.0", # For basic agent architecture "litellm>=1.74.3,<=1.82.6", # For model inference "loguru==0.7.3", # For better logging "opentelemetry-exporter-otlp==1.37.0",