Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions config.yaml.full
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 视频编辑能力组 |
Expand Down
53 changes: 53 additions & 0 deletions docs/docs/tools/feishu-channel.md
Original file line number Diff line number Diff line change
@@ -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` 覆盖默认映射逻辑。
32 changes: 32 additions & 0 deletions docs/examples/channel/feishu_bot.py
Original file line number Diff line number Diff line change
@@ -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())
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ nav:
# - 最佳实践(记忆相关): memory/best-practice-memory.md
- 连接能力源——工具:
- 内置工具: tools/builtin.md
- 飞书 Channel 扩展: tools/feishu-channel.md
- 自定义工具: tools/function.md
- 护栏工具: tools/guardrail.md
- 连接数据源——知识库:
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ 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
"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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -97,4 +98,4 @@ include-package-data = true
exclude = [
"veadk/integrations/ve_faas/template/*",
"veadk/integrations/ve_faas/web_template/*"
]
]
33 changes: 17 additions & 16 deletions tests/realtime/test_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
41 changes: 27 additions & 14 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"})
Expand Down
Loading
Loading