diff --git a/README.md b/README.md index 1bef4554..2351ac37 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,23 @@ res = asyncio.run(agent.run("hello!")) print(res) ``` +## AgentKit application + +Use the shared AgentKit application factory when your project needs AgentKit +APIs, VeADK's bundled Web UI, health checks, and agent-topology endpoints. This +keeps platform routes and lifecycle code out of your agent module: + +```python +from veadk import Agent +from veadk.integrations.agentkit import create_agentkit_app + +root_agent = Agent(name="customer_support") +app = create_agentkit_app(root_agent) +``` + +See [`examples/generated_agentkit_project`](examples/generated_agentkit_project) +for a complete generated project. + ## 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. diff --git a/docs/content/docs/framework/agentkit.en.mdx b/docs/content/docs/framework/agentkit.en.mdx index 02b1380d..1a9a7cfc 100644 --- a/docs/content/docs/framework/agentkit.en.mdx +++ b/docs/content/docs/framework/agentkit.en.mdx @@ -9,6 +9,29 @@ Besides deploying to VeFaaS, you can deploy your agent to the Volcengine **Agent - VeADK is installed (it bundles the `agentkit` dependency). - Volcengine access credentials (Access Key / Secret Key) are configured in the `.env` file at your project root. +## Shared application component + +New projects generated by Studio contain only agent business logic and a small +`app.py`. `veadk.integrations.agentkit` provides the AgentKit APIs, health +check, agent topology, bundled Web UI, default short-term memory, and optional +Feishu lifecycle: + +```python +from agents.my_agent.agent import AGENT_DISPLAY_NAMES, root_agent +from veadk.integrations.agentkit import create_agentkit_app + +app = create_agentkit_app( + root_agent, + AGENT_DISPLAY_NAMES, +) +``` + +The resulting service exposes AgentKit conversation APIs together with +`/ping`, `/web/agent-info/{app_name}`, `/web/agent-graph`, and the bundled Web +UI. See +[`examples/generated_agentkit_project`](https://github.com/volcengine/veadk-python/tree/main/examples/generated_agentkit_project) +for a complete generated project. + ## Steps diff --git a/docs/content/docs/framework/agentkit.mdx b/docs/content/docs/framework/agentkit.mdx index 71c72e5a..11fd50fe 100644 --- a/docs/content/docs/framework/agentkit.mdx +++ b/docs/content/docs/framework/agentkit.mdx @@ -9,6 +9,26 @@ title: "部署到 AgentKit" - 已安装 VeADK(包含 `agentkit` 依赖)。 - 已在项目根目录的 `.env` 文件中配置火山引擎访问凭据(Access Key / Secret Key)。 +## 公共应用组件 + +通过 Studio 新生成的项目只保留 Agent 业务代码和一个精简的 `app.py`。AgentKit +接口、健康检查、Agent 拓扑、内置 Web UI、短期记忆默认配置和可选的飞书生命周期 +由 `veadk.integrations.agentkit` 统一提供: + +```python +from agents.my_agent.agent import AGENT_DISPLAY_NAMES, root_agent +from veadk.integrations.agentkit import create_agentkit_app + +app = create_agentkit_app( + root_agent, + AGENT_DISPLAY_NAMES, +) +``` + +服务会提供 AgentKit 对话接口,以及 `/ping`、`/web/agent-info/{app_name}`、 +`/web/agent-graph` 和内置 Web UI。完整生成结果可参考 +[`examples/generated_agentkit_project`](https://github.com/volcengine/veadk-python/tree/main/examples/generated_agentkit_project)。 + ## 步骤 diff --git a/examples/generated_agentkit_project/.env.example b/examples/generated_agentkit_project/.env.example new file mode 100644 index 00000000..62843d9e --- /dev/null +++ b/examples/generated_agentkit_project/.env.example @@ -0,0 +1,8 @@ +# Copy this file to .env and replace the placeholder. + +# Required Volcengine Ark API key. +MODEL_AGENT_API_KEY=your-ark-api-key +# Model configuration. +MODEL_AGENT_NAME=doubao-seed-1-6-250615 +MODEL_AGENT_PROVIDER=openai +MODEL_AGENT_API_BASE=https://ark.cn-beijing.volces.com/api/v3/ diff --git a/examples/generated_agentkit_project/README.md b/examples/generated_agentkit_project/README.md new file mode 100644 index 00000000..50944b59 --- /dev/null +++ b/examples/generated_agentkit_project/README.md @@ -0,0 +1,19 @@ +# Generated AgentKit project + +This example shows the output of VeADK Studio for a root customer-support Agent +with one order assistant. Business logic lives under `agents/`; `app.py` only +connects the Agent to VeADK's shared AgentKit application component. + +## Run + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +cp .env.example .env +# Set MODEL_AGENT_API_KEY in .env. +python app.py +``` + +The service listens on `0.0.0.0:8000` by default. Override it with the `HOST` +and `PORT` environment variables. diff --git a/examples/generated_agentkit_project/agents/__init__.py b/examples/generated_agentkit_project/agents/__init__.py new file mode 100644 index 00000000..7f463206 --- /dev/null +++ b/examples/generated_agentkit_project/agents/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/examples/generated_agentkit_project/agents/customer_support/__init__.py b/examples/generated_agentkit_project/agents/customer_support/__init__.py new file mode 100644 index 00000000..0f830963 --- /dev/null +++ b/examples/generated_agentkit_project/agents/customer_support/__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 .agent import AGENT_DISPLAY_NAMES, root_agent + +__all__ = ["AGENT_DISPLAY_NAMES", "root_agent"] diff --git a/examples/generated_agentkit_project/agents/customer_support/agent.py b/examples/generated_agentkit_project/agents/customer_support/agent.py new file mode 100644 index 00000000..fdddf810 --- /dev/null +++ b/examples/generated_agentkit_project/agents/customer_support/agent.py @@ -0,0 +1,42 @@ +# 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 import Agent + +INSTRUCTION_AGENT = ( + """Route order questions to the order assistant and answer general questions.""" +) + +INSTRUCTION_AGENT_SUB_1 = """Help the user check and understand an order.""" + +agent_sub_1 = Agent( + name="order_assistant", + description="Handles order questions.", + instruction=INSTRUCTION_AGENT_SUB_1, +) + +agent = Agent( + name="customer_support", + description="A generated customer-support multi-agent example.", + instruction=INSTRUCTION_AGENT, + sub_agents=[agent_sub_1], +) + +AGENT_DISPLAY_NAMES = { + "customer_support": "customer_support", + "order_assistant": "order_assistant", +} + +# ADK requires the top-level agent to be exported as root_agent. +root_agent = agent diff --git a/examples/generated_agentkit_project/app.py b/examples/generated_agentkit_project/app.py new file mode 100644 index 00000000..4cabe725 --- /dev/null +++ b/examples/generated_agentkit_project/app.py @@ -0,0 +1,25 @@ +# 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 agents.customer_support.agent import AGENT_DISPLAY_NAMES, root_agent +from veadk.integrations.agentkit import create_agentkit_app, run_agentkit_app + +app = create_agentkit_app( + root_agent, + AGENT_DISPLAY_NAMES, + enable_feishu=False, +) + +if __name__ == "__main__": + run_agentkit_app(app) diff --git a/examples/generated_agentkit_project/requirements.txt b/examples/generated_agentkit_project/requirements.txt new file mode 100644 index 00000000..0798144b --- /dev/null +++ b/examples/generated_agentkit_project/requirements.txt @@ -0,0 +1,4 @@ +veadk-python>=1.0.5 +agentkit-sdk-python +google-adk +starlette<1.0.0 diff --git a/tests/cli/test_generated_agent_backend_codegen_extended.py b/tests/cli/test_generated_agent_backend_codegen_extended.py index 631457c7..66b0b69d 100644 --- a/tests/cli/test_generated_agent_backend_codegen_extended.py +++ b/tests/cli/test_generated_agent_backend_codegen_extended.py @@ -47,27 +47,26 @@ ) -# These hashes were produced from the legacy frontend generator before backend -# codegen became the trusted implementation. They intentionally lock the full -# generated file contents, not just Python syntax or selected snippets. +# These hashes lock the complete generated project contents, not just Python +# syntax or selected snippets. _MINIMAL_FRONTEND_GOLDEN = { - "app.py": "76fb40c6016bf2b70e35012d050b6a0e5be36233b6044e83e1002a3f71150bfb", - "agents/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "agents/demo_agent/agent.py": "6e5bbd448439c7f50f5400227680130e897dd71e8251cb4eb4594c98f4889589", - "agents/demo_agent/__init__.py": "acb6368da255ff70d6760c00085c893aa0b2d973768d293a22bb7181fb1e3448", + "app.py": "c7807c570167793fc8c5a8a72e9f3c32aac0820db3098cb52edb1488c34a8e5f", + "agents/__init__.py": "a6449a6cac3bfda8b834ea39ea95ca2f8d0471ac480e1e876313d7398eea59ba", + "agents/demo_agent/agent.py": "b2d22094a8ea61e8ab6e2b633d7695c5fa5e883f03516cb8771e7ec00be0fe1f", + "agents/demo_agent/__init__.py": "62d651c229ddd771cf0cc0a8b0e05e96b739a737fe71e41fe8bf1df484150c36", ".env.example": "1cdb6e1bfe38616d5d46095ba88ba76a0c189f3d2999bf0dd23b7145ce103ab2", - "requirements.txt": "a7bb29cb47b916a81b626907fcdf84eed525ca22b4214ddc82f96a5ba87c8cc8", - "README.md": "16cbec845b595949c071f3bcf4c056d862b9e2277c00e5d23649b5540dfde83e", + "requirements.txt": "9a04e5f16e94d5e751681082776f1c99f13da7a577c8753c3835e0ea507245e4", + "README.md": "a34208314cf9061c02662028d7a9dd97448e6b73c1d732cb4aeaa8f70dbbc684", } _FULL_FRONTEND_GOLDEN = { - "app.py": "bce061ef2a9db0ba8fa2c82c94ae682f166d96bca4d580e99fcaacca71b9a682", - "agents/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "agents/full_agent/agent.py": "d77ee467c4895b1a160927fb551a65d424b68319fc2941014dc5d499edbace68", - "agents/full_agent/__init__.py": "acb6368da255ff70d6760c00085c893aa0b2d973768d293a22bb7181fb1e3448", + "app.py": "a9903cf7e095733e9b8658182a0954a81d8a98b431f8ab995ce3818950127006", + "agents/__init__.py": "a6449a6cac3bfda8b834ea39ea95ca2f8d0471ac480e1e876313d7398eea59ba", + "agents/full_agent/agent.py": "77c6cc42f8f9a99aba060fc93a7a615655b897e6136d6f5325f035e87edce141", + "agents/full_agent/__init__.py": "62d651c229ddd771cf0cc0a8b0e05e96b739a737fe71e41fe8bf1df484150c36", ".env.example": "cb35eed98b4155c755df934f61ca6760293d59508de0a6090632e44501f82748", - "requirements.txt": "5230e5c9a20b97dc95cc753247b4240d7401d9f9b46aa62da851c91552061ba7", - "README.md": "ce6e5ada2031657b5de320465a34cb8c066c6c4181ab111dfb40299d3ec0bcd0", + "requirements.txt": "65b301155863e56165c5777301c3476d0c0b68e569fff4450f454e36fd66225d", + "README.md": "1bf4dc889c7d1076f50784d253b53412ba7c49bcb69a5d948f9092dbbecb18ac", } @@ -184,8 +183,21 @@ def test_codegen_preserves_agent_display_names_for_topology() -> None: assert "'agent': '客服智能体'" in agent_py assert "'agent_sub_1': '订单助手'" in agent_py - assert '"id": agent_id' in app_py - assert '"name": AGENT_DISPLAY_NAMES.get(agent_id, agent_id)' in app_py + assert "create_agentkit_app(" in app_py + assert "AGENT_DISPLAY_NAMES" in app_py + assert 'app.get("/web/agent-info' not in app_py + assert len(app_py.splitlines()) == 25 + + +def test_codegen_enables_feishu_without_exposing_lifecycle_code() -> None: + project = generate_project_from_draft( + AgentDraft(name="demo", deployment={"feishuEnabled": True}) + ) + app_py = _file_map(project)["app.py"] + + assert "enable_feishu=True" in app_py + assert "FeishuChannelExtension" not in app_py + assert "asynccontextmanager" not in app_py def test_frontend_complete_shape_is_accepted_and_unknown_field_is_rejected() -> None: diff --git a/tests/integrations/agentkit/test_app.py b/tests/integrations/agentkit/test_app.py new file mode 100644 index 00000000..5fedf9e2 --- /dev/null +++ b/tests/integrations/agentkit/test_app.py @@ -0,0 +1,191 @@ +# 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 __future__ import annotations + +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from google.adk.agents.base_agent import BaseAgent + +import veadk +import veadk.integrations.agentkit.app as agentkit_app + + +class _FakeAgentServer: + instances: list[_FakeAgentServer] = [] + + def __init__(self, agent: BaseAgent, short_term_memory: object) -> None: + self.agent = agent + self.short_term_memory = short_term_memory + self.app = FastAPI() + self.run_kwargs: dict[str, Any] | None = None + self.instances.append(self) + + def run(self, **kwargs: Any) -> None: + self.run_kwargs = kwargs + + +class _FakeShortTermMemory: + def __init__(self, backend: str) -> None: + self.backend = backend + + +@pytest.fixture(autouse=True) +def fake_agentkit_server(monkeypatch: pytest.MonkeyPatch) -> None: + _FakeAgentServer.instances.clear() + monkeypatch.setattr(agentkit_app, "AgentkitAgentServerApp", _FakeAgentServer) + monkeypatch.setattr(agentkit_app, "ShortTermMemory", _FakeShortTermMemory) + + +def _root_agent() -> BaseAgent: + child = SimpleNamespace( + name="agent_sub_1", + description="Handles orders", + model="child-model", + tools=[], + sub_agents=[], + ) + root = SimpleNamespace( + name="agent", + description="Customer support", + model=SimpleNamespace(model="doubao-model"), + tools=[SimpleNamespace(name="search_orders")], + sub_agents=[child], + ) + return cast(BaseAgent, root) + + +def test_create_agentkit_app_preserves_platform_route_contract() -> None: + app = agentkit_app.create_agentkit_app( + _root_agent(), + {"agent": "客服智能体", "agent_sub_1": "订单助手"}, + ) + + server = _FakeAgentServer.instances[-1] + assert isinstance(server.short_term_memory, _FakeShortTermMemory) + assert server.short_term_memory.backend == "local" + + client = TestClient(app) + assert client.get("/ping").json() == {"status": "ok"} + info = client.get("/web/agent-info/agent") + assert info.status_code == 200 + assert info.json() == { + "id": "agent", + "name": "客服智能体", + "description": "Customer support", + "type": "llm", + "model": "doubao-model", + "tools": ["search_orders"], + "subAgents": ["订单助手"], + "graph": { + "id": "agent", + "name": "客服智能体", + "description": "Customer support", + "type": "llm", + "model": "doubao-model", + "tools": ["search_orders"], + "children": [ + { + "id": "agent_sub_1", + "name": "订单助手", + "description": "Handles orders", + "type": "llm", + "model": "child-model", + "tools": [], + "children": [], + } + ], + }, + } + assert client.get("/web/agent-info/unknown").status_code == 404 + assert client.get("/web/agent-graph").json()["graph"] == info.json()["graph"] + + +def test_create_agentkit_app_reuses_agent_memory_and_configures_feishu( + monkeypatch: pytest.MonkeyPatch, +) -> None: + memory = object() + root_agent = _root_agent() + setattr(root_agent, "short_term_memory", memory) + configured: list[tuple[FastAPI, BaseAgent, object]] = [] + + monkeypatch.setattr( + agentkit_app, + "_configure_feishu_lifecycle", + lambda app, agent, short_term_memory: configured.append( + (app, agent, short_term_memory) + ), + ) + + app = agentkit_app.create_agentkit_app(root_agent, enable_feishu=True) + + assert _FakeAgentServer.instances[-1].short_term_memory is memory + assert configured == [(app, root_agent, memory)] + + +def test_feishu_lifecycle_starts_and_stops_with_application( + monkeypatch: pytest.MonkeyPatch, +) -> None: + runners: list[dict[str, Any]] = [] + events: list[str] = [] + + class _FakeRunner: + def __init__(self, **kwargs: Any) -> None: + runners.append(kwargs) + + async def fake_start(app: FastAPI, runner: object) -> None: + del app, runner + events.append("start") + + async def fake_stop(app: FastAPI) -> None: + del app + events.append("stop") + + monkeypatch.setattr(veadk, "Runner", _FakeRunner) + monkeypatch.setattr(agentkit_app, "_start_feishu_channel", fake_start) + monkeypatch.setattr(agentkit_app, "_stop_feishu_channel", fake_stop) + root_agent = _root_agent() + + app = agentkit_app.create_agentkit_app(root_agent, enable_feishu=True) + with TestClient(app): + assert events == ["start"] + + assert events == ["start", "stop"] + assert runners[0]["agent"] is root_agent + assert runners[0]["app_name"] == "agent" + assert isinstance(runners[0]["short_term_memory"], _FakeShortTermMemory) + + +def test_run_agentkit_app_uses_explicit_and_environment_configuration( + monkeypatch: pytest.MonkeyPatch, +) -> None: + app = agentkit_app.create_agentkit_app(_root_agent()) + server = _FakeAgentServer.instances[-1] + + agentkit_app.run_agentkit_app(app, host="127.0.0.1", port=9000) + assert server.run_kwargs == {"host": "127.0.0.1", "port": 9000} + + monkeypatch.setenv("HOST", "0.0.0.0") + monkeypatch.setenv("PORT", "8080") + agentkit_app.run_agentkit_app(app) + assert server.run_kwargs == {"host": "0.0.0.0", "port": 8080} + + +def test_run_agentkit_app_rejects_unmanaged_app() -> None: + with pytest.raises(ValueError, match="create_agentkit_app"): + agentkit_app.run_agentkit_app(FastAPI()) diff --git a/veadk/cli/generated_agent_codegen.py b/veadk/cli/generated_agent_codegen.py index fe800431..b7997496 100644 --- a/veadk/cli/generated_agent_codegen.py +++ b/veadk/cli/generated_agent_codegen.py @@ -31,6 +31,21 @@ EnvVar, ) +_PYTHON_LICENSE_HEADER = """# 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. +""" + class GeneratedFile(BaseModel): model_config = ConfigDict(extra="forbid") @@ -527,7 +542,7 @@ def render_requirements(extras: set[str], include_feishu_channel: bool) -> str: all_extras.add("extensions") unique_extras = sorted(all_extras) extras_str = f"[{','.join(unique_extras)}]" if unique_extras else "" - pkg = f"veadk-python{extras_str}>=1.0.4" + pkg = f"veadk-python{extras_str}>=1.0.5" packages = [pkg, "agentkit-sdk-python", "google-adk", "starlette<1.0.0"] return "\n".join(packages) + "\n" @@ -546,7 +561,7 @@ def render_readme(name: str, draft: AgentDraft) -> str: "python app.py", "```", "", - "`app.py` 使用 AgentKit AgentServerApp 包裹 `root_agent`,监听 `0.0.0.0:8000`。", + "`app.py` 通过 VeADK 的 AgentKit 公共组件发布 `root_agent`,监听 `0.0.0.0:8000`。", "", ] if draft.deployment.feishuEnabled: @@ -562,350 +577,18 @@ def render_readme(name: str, draft: AgentDraft) -> str: def _render_app_py(pkg: str, feishu_channel_enabled: bool) -> str: - feishu_imports = ( - "\nimport asyncio\nimport inspect\nimport threading\nimport traceback\n" - "from contextlib import asynccontextmanager\n" - "from veadk.extensions import FeishuChannelExtension" - if feishu_channel_enabled - else "" - ) - feishu_helpers = _FEISHU_HELPERS if feishu_channel_enabled else "" - feishu_runner_import = ( - " from veadk import Runner\n" if feishu_channel_enabled else "" - ) - feishu_runner = ( - " runner = Runner(\n" - " agent=root_agent,\n" - ' app_name=getattr(root_agent, "name", "") or "agent",\n' - " short_term_memory=short_term_memory,\n" - " )\n" - if feishu_channel_enabled - else "" - ) - feishu_lifespan = _FEISHU_LIFESPAN if feishu_channel_enabled else "" - feishu_config = ( - f"FEISHU_CHANNEL_ENABLED = True\n\n{feishu_helpers}" - if feishu_channel_enabled - else "" - ) - return f"""import os{feishu_imports} -from pathlib import Path -from agentkit.apps import AgentkitAgentServerApp -from fastapi.staticfiles import StaticFiles -from veadk.memory.short_term_memory import ShortTermMemory + return f"""{_PYTHON_LICENSE_HEADER} from agents.{pkg}.agent import AGENT_DISPLAY_NAMES, root_agent +from veadk.integrations.agentkit import create_agentkit_app, run_agentkit_app -# Deployment configuration -HOST = os.getenv("HOST", "0.0.0.0") -PORT = int(os.getenv("PORT", "8000")) -{feishu_config} -def build_app(): - \"\"\"Build AgentKit AgentServerApp for deployment.\"\"\" - import veadk -{feishu_runner_import} WEBUI_DIR = Path(veadk.__file__).resolve().parent / "webui" - - # AgentKit's AgentServerApp exposes the ADK-compatible API surface - # (/list-apps, /run, /run_sse, sessions) expected by AgentKit runtime tests. - short_term_memory = getattr(root_agent, "short_term_memory", None) or ShortTermMemory( - backend="local" - ) -{feishu_runner} agent_server_app = AgentkitAgentServerApp( - agent=root_agent, - short_term_memory=short_term_memory, - ) - app = agent_server_app.app - -{feishu_lifespan} # Add health check endpoint - @app.get("/ping") - def ping() -> dict[str, str]: - return {{"status": "ok"}} - - # Agent-structure introspection (data plane), consumed by the VeADK web - # UI's "管理 Agent" view to show this runtime's agent name + sub-agent tree. - from fastapi import HTTPException as _HTTPException - - def _agent_type(a: object) -> str: - try: - from google.adk.agents import LoopAgent, ParallelAgent, SequentialAgent - - if isinstance(a, LoopAgent): - return "loop" - if isinstance(a, SequentialAgent): - return "sequential" - if isinstance(a, ParallelAgent): - return "parallel" - except Exception: - pass - try: - from google.adk.agents.remote_a2a_agent import RemoteA2aAgent - - if isinstance(a, RemoteA2aAgent): - return "a2a" - except Exception: - pass - return "llm" - - def _model_name(m: object) -> str: - if isinstance(m, str): - return m - return str(getattr(m, "model", None) or type(m).__name__) - - def _tool_label(t: object) -> str: - name = getattr(t, "name", None) or getattr(t, "__name__", None) - return str(name or type(t).__name__) - - def _agent_node(a: object, depth: int = 0) -> dict: - children = [] - if depth < 8: - children = [_agent_node(s, depth + 1) for s in getattr(a, "sub_agents", []) or []] - agent_id = getattr(a, "name", "") or "" - return {{ - "id": agent_id, - "name": AGENT_DISPLAY_NAMES.get(agent_id, agent_id), - "description": getattr(a, "description", "") or "", - "type": _agent_type(a), - "model": _model_name(getattr(a, "model", "")), - "tools": [_tool_label(t) for t in getattr(a, "tools", []) or []], - "children": children, - }} - - @app.get("/web/agent-info/{{app_name}}") - def agent_info(app_name: str) -> dict: - expected_name = getattr(root_agent, "name", "") or "" - if app_name != expected_name: - raise _HTTPException(status_code=404, detail="unknown agent: " + app_name) - return {{ - "id": expected_name, - "name": AGENT_DISPLAY_NAMES.get(expected_name, expected_name or app_name), - "description": getattr(root_agent, "description", "") or "", - "type": _agent_type(root_agent), - "model": _model_name(getattr(root_agent, "model", "")), - "tools": [_tool_label(t) for t in getattr(root_agent, "tools", []) or []], - "subAgents": [ - AGENT_DISPLAY_NAMES.get(getattr(s, "name", ""), getattr(s, "name", "")) - for s in getattr(root_agent, "sub_agents", []) or [] - ], - "graph": _agent_node(root_agent), - }} - - @app.get("/web/agent-graph") - def agent_graph() -> dict: - # Single introspection endpoint on the main agent: returns this runtime's - # root agent + recursive sub-agent tree, with no /list-apps discovery - # needed. Used by the VeADK "管理 Agent" view. - return {{ - "id": getattr(root_agent, "name", "") or "", - "name": AGENT_DISPLAY_NAMES.get( - getattr(root_agent, "name", "") or "", - getattr(root_agent, "name", "") or "", - ), - "description": getattr(root_agent, "description", "") or "", - "type": _agent_type(root_agent), - "model": _model_name(getattr(root_agent, "model", "")), - "tools": [_tool_label(t) for t in getattr(root_agent, "tools", []) or []], - "graph": _agent_node(root_agent), - }} - - # Serve the bundled VeADK web UI without taking over "/", which is reserved - # by AgentServerApp for the A2A protocol surface. - if (WEBUI_DIR / "index.html").is_file(): - if (WEBUI_DIR / "assets").is_dir(): - app.mount( - "/assets", - StaticFiles(directory=str(WEBUI_DIR / "assets")), - name="webui-assets", - ) - - from fastapi.responses import FileResponse as _FileResponse - - @app.get("/") - @app.get("/webui") - @app.get("/webui/{{path:path}}") - def webui(path: str = ""): - return _FileResponse(WEBUI_DIR / "index.html") - - # AgentServerApp mounts A2A at "/", so routes added after construction must - # be moved before that root mount or they will be shadowed. - _priority_paths = {{ - "/", - "/ping", - "/web/agent-info/{{app_name}}", - "/web/agent-graph", - "/assets", - "/webui", - "/webui/{{path:path}}", - }} - _priority_routes = [ - r for r in app.router.routes if getattr(r, "path", None) in _priority_paths - ] - if _priority_routes: - app.router.routes[:] = _priority_routes + [ - r for r in app.router.routes if r not in _priority_routes - ] - - return agent_server_app, app - -agent_server_app, app = build_app() +app = create_agentkit_app( + root_agent, + AGENT_DISPLAY_NAMES, + enable_feishu={feishu_channel_enabled!r}, +) if __name__ == "__main__": - agent_server_app.run(host=HOST, port=PORT) -""" - - -_FEISHU_HELPERS = """def _get_feishu_channel_method(channel, names): - raw_channel = getattr(channel, "channel", None) - for target in (raw_channel, channel): - if target is None: - continue - for name in names: - method = getattr(target, name, None) - if method is not None: - return method - return None - -def _call_feishu_channel_method(loop, method): - result = method() - if inspect.isawaitable(result): - return loop.run_until_complete(result) - return result - -def _connect_feishu_channel(loop, channel) -> None: - connect = _get_feishu_channel_method(channel, ("start", "connect")) - if connect is None: - raise AttributeError("Feishu channel has no start/connect method") - return _call_feishu_channel_method(loop, connect) - -def _disconnect_feishu_channel(loop, channel) -> None: - disconnect = _get_feishu_channel_method(channel, ("stop", "disconnect")) - if disconnect is None: - return None - return _call_feishu_channel_method(loop, disconnect) - -def _stop_feishu_channel_from_lifespan(channel) -> None: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return _disconnect_feishu_channel(loop, channel) - finally: - asyncio.set_event_loop(None) - loop.close() - -def _build_feishu_channel(runner, app_id, app_secret): - return FeishuChannelExtension( - runner=runner, - app_id=app_id, - app_secret=app_secret, - channel_kwargs={ - "transport": "ws", - }, - streaming=False, - reactions=False, - ) - -def _run_feishu_channel(runner, app_id, app_secret, stop_event, state) -> None: - loop = asyncio.new_event_loop() - state["loop"] = loop - asyncio.set_event_loop(loop) - try: - while not stop_event.is_set(): - channel = None - try: - channel = _build_feishu_channel(runner, app_id, app_secret) - state["channel"] = channel - print("feishu channel connecting in dedicated thread", flush=True) - _connect_feishu_channel(loop, channel) - print("feishu channel disconnected; reconnecting in 5s", flush=True) - except Exception as exc: - if channel is None: - print( - f"feishu channel initialization failed: {type(exc).__name__}: {exc}; reconnecting in 5s", - flush=True, - ) - print(traceback.format_exc(), flush=True) - else: - print( - f"feishu channel connect failed: {type(exc).__name__}: {exc}; reconnecting in 5s", - flush=True, - ) - finally: - if channel is not None: - try: - _disconnect_feishu_channel(loop, channel) - except Exception as exc: - print( - f"feishu channel disconnect failed: {type(exc).__name__}: {exc}", - flush=True, - ) - finally: - if state.get("channel") is channel: - state["channel"] = None - stop_event.wait(5) - finally: - asyncio.set_event_loop(None) - state["loop"] = None - loop.close() - -async def _start_feishu_channel(app, runner) -> None: - if not FEISHU_CHANNEL_ENABLED: - return - - app_id = os.getenv("FEISHU_APP_ID") - app_secret = os.getenv("FEISHU_APP_SECRET") - if not app_id or not app_secret: - print( - "feishu channel disabled: FEISHU_APP_ID or FEISHU_APP_SECRET is missing", - flush=True, - ) - return - - app.state.feishu_channel_state = {"channel": None, "loop": None} - app.state.feishu_channel_stop_event = threading.Event() - app.state.feishu_channel_thread = threading.Thread( - target=_run_feishu_channel, - args=( - runner, - app_id, - app_secret, - app.state.feishu_channel_stop_event, - app.state.feishu_channel_state, - ), - name="feishu-channel", - daemon=True, - ) - app.state.feishu_channel_thread.start() - print("feishu channel background thread started", flush=True) - -async def _stop_feishu_channel(app) -> None: - stop_event = getattr(app.state, "feishu_channel_stop_event", None) - if stop_event is not None: - stop_event.set() - state = getattr(app.state, "feishu_channel_state", None) or {} - channel = state.get("channel") - if channel is not None: - await asyncio.to_thread(_stop_feishu_channel_from_lifespan, channel) - thread = getattr(app.state, "feishu_channel_thread", None) - if thread is not None: - await asyncio.to_thread(thread.join, 2) - if thread.is_alive(): - print( - "feishu channel background thread did not stop within 2s", - flush=True, - ) -""" - - -_FEISHU_LIFESPAN = """ original_lifespan = app.router.lifespan_context - - @asynccontextmanager - async def lifespan(fastapi_app): - async with original_lifespan(fastapi_app): - await _start_feishu_channel(fastapi_app, runner) - try: - yield - finally: - await _stop_feishu_channel(fastapi_app) - - app.router.lifespan_context = lifespan + run_agentkit_app(app) """ @@ -939,18 +622,19 @@ def generate_project_from_draft(draft: AgentDraft) -> GeneratedProject: + f"\n\nAGENT_DISPLAY_NAMES = {acc.agent_display_names!r}\n" + "\n# ADK 加载器要求:顶层 agent 必须命名为 root_agent\nroot_agent = agent\n" ) - agent_py = f"{import_block}\n\n{agent_definition}" + agent_py = f"{_PYTHON_LICENSE_HEADER}\n{import_block}\n\n{agent_definition}" app_py = _render_app_py(pkg, feishu_channel_enabled) files = [ GeneratedFile(path="app.py", content=app_py), # Top-level agents package marker so `from agents..agent import # root_agent` resolves when the container runs `python -m app`. - GeneratedFile(path="agents/__init__.py", content=""), + GeneratedFile(path="agents/__init__.py", content=_PYTHON_LICENSE_HEADER), GeneratedFile(path=f"agents/{pkg}/agent.py", content=agent_py), GeneratedFile( path=f"agents/{pkg}/__init__.py", content=( + f"{_PYTHON_LICENSE_HEADER}\n" "from .agent import AGENT_DISPLAY_NAMES, root_agent\n\n" '__all__ = ["AGENT_DISPLAY_NAMES", "root_agent"]\n' ), diff --git a/veadk/integrations/agentkit/__init__.py b/veadk/integrations/agentkit/__init__.py new file mode 100644 index 00000000..6ae46c91 --- /dev/null +++ b/veadk/integrations/agentkit/__init__.py @@ -0,0 +1,22 @@ +# 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. + +"""Public helpers for serving VeADK agents on AgentKit.""" + +from veadk.integrations.agentkit.app import ( + create_agentkit_app, + run_agentkit_app, +) + +__all__ = ["create_agentkit_app", "run_agentkit_app"] diff --git a/veadk/integrations/agentkit/app.py b/veadk/integrations/agentkit/app.py new file mode 100644 index 00000000..d688b9e2 --- /dev/null +++ b/veadk/integrations/agentkit/app.py @@ -0,0 +1,423 @@ +# 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. + +"""Build an AgentKit application around a VeADK agent.""" + +from __future__ import annotations + +import asyncio +import inspect +import os +import threading +import traceback +from collections.abc import Callable, Mapping +from contextlib import asynccontextmanager +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +from agentkit.apps import AgentkitAgentServerApp +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from google.adk.agents import LoopAgent, ParallelAgent, SequentialAgent +from google.adk.agents.base_agent import BaseAgent +from google.adk.agents.remote_a2a_agent import RemoteA2aAgent + +from veadk.memory.short_term_memory import ShortTermMemory + +if TYPE_CHECKING: + from veadk.runner import Runner + +_MAX_AGENT_GRAPH_DEPTH = 8 +_SERVER_STATE_KEY = "_veadk_agentkit_server" + + +def _agent_type(agent: object) -> str: + if isinstance(agent, LoopAgent): + return "loop" + if isinstance(agent, SequentialAgent): + return "sequential" + if isinstance(agent, ParallelAgent): + return "parallel" + if isinstance(agent, RemoteA2aAgent): + return "a2a" + return "llm" + + +def _model_name(model: object) -> str: + if isinstance(model, str): + return model + return str(getattr(model, "model", None) or type(model).__name__) + + +def _tool_label(tool: object) -> str: + name = getattr(tool, "name", None) or getattr(tool, "__name__", None) + return str(name or type(tool).__name__) + + +def _display_name( + agent_id: str, + display_names: Mapping[str, str], +) -> str: + return display_names.get(agent_id, agent_id) + + +def _agent_node( + agent: object, + display_names: Mapping[str, str], + depth: int = 0, +) -> dict[str, Any]: + children: list[dict[str, Any]] = [] + if depth < _MAX_AGENT_GRAPH_DEPTH: + children = [ + _agent_node(child, display_names, depth + 1) + for child in getattr(agent, "sub_agents", []) or [] + ] + agent_id = str(getattr(agent, "name", "") or "") + return { + "id": agent_id, + "name": _display_name(agent_id, display_names), + "description": getattr(agent, "description", "") or "", + "type": _agent_type(agent), + "model": _model_name(getattr(agent, "model", "")), + "tools": [_tool_label(tool) for tool in getattr(agent, "tools", []) or []], + "children": children, + } + + +def _get_feishu_channel_method( + channel: object, + names: tuple[str, ...], +) -> Callable[[], Any] | None: + raw_channel = getattr(channel, "channel", None) + for target in (raw_channel, channel): + if target is None: + continue + for name in names: + method = getattr(target, name, None) + if callable(method): + return method + return None + + +def _call_feishu_channel_method( + loop: asyncio.AbstractEventLoop, + method: Callable[[], Any], +) -> Any: + result = method() + if inspect.isawaitable(result): + return loop.run_until_complete(result) + return result + + +def _connect_feishu_channel( + loop: asyncio.AbstractEventLoop, + channel: object, +) -> Any: + connect = _get_feishu_channel_method(channel, ("start", "connect")) + if connect is None: + raise AttributeError("Feishu channel has no start/connect method") + return _call_feishu_channel_method(loop, connect) + + +def _disconnect_feishu_channel( + loop: asyncio.AbstractEventLoop, + channel: object, +) -> Any: + disconnect = _get_feishu_channel_method(channel, ("stop", "disconnect")) + if disconnect is None: + return None + return _call_feishu_channel_method(loop, disconnect) + + +def _stop_feishu_channel_from_lifespan(channel: object) -> None: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + _disconnect_feishu_channel(loop, channel) + finally: + asyncio.set_event_loop(None) + loop.close() + + +def _build_feishu_channel(runner: Runner, app_id: str, app_secret: str) -> object: + from veadk.extensions import FeishuChannelExtension + + return FeishuChannelExtension( + runner=runner, + app_id=app_id, + app_secret=app_secret, + channel_kwargs={"transport": "ws"}, + streaming=False, + reactions=False, + ) + + +def _run_feishu_channel( + runner: Runner, + app_id: str, + app_secret: str, + stop_event: threading.Event, + state: dict[str, Any], +) -> None: + loop = asyncio.new_event_loop() + state["loop"] = loop + asyncio.set_event_loop(loop) + try: + while not stop_event.is_set(): + channel = None + try: + channel = _build_feishu_channel(runner, app_id, app_secret) + state["channel"] = channel + print("feishu channel connecting in dedicated thread", flush=True) + _connect_feishu_channel(loop, channel) + print("feishu channel disconnected; reconnecting in 5s", flush=True) + except Exception as exc: # The channel reconnects after transport errors. + stage = "initialization" if channel is None else "connect" + print( + f"feishu channel {stage} failed: " + f"{type(exc).__name__}: {exc}; reconnecting in 5s", + flush=True, + ) + if channel is None: + print(traceback.format_exc(), flush=True) + finally: + if channel is not None: + try: + _disconnect_feishu_channel(loop, channel) + except Exception as exc: # Cleanup must not stop reconnection. + print( + "feishu channel disconnect failed: " + f"{type(exc).__name__}: {exc}", + flush=True, + ) + finally: + if state.get("channel") is channel: + state["channel"] = None + stop_event.wait(5) + finally: + asyncio.set_event_loop(None) + state["loop"] = None + loop.close() + + +async def _start_feishu_channel(app: FastAPI, runner: Runner) -> None: + app_id = os.getenv("FEISHU_APP_ID") + app_secret = os.getenv("FEISHU_APP_SECRET") + if not app_id or not app_secret: + print( + "feishu channel disabled: FEISHU_APP_ID or FEISHU_APP_SECRET is missing", + flush=True, + ) + return + + app.state.feishu_channel_state = {"channel": None, "loop": None} + app.state.feishu_channel_stop_event = threading.Event() + app.state.feishu_channel_thread = threading.Thread( + target=_run_feishu_channel, + args=( + runner, + app_id, + app_secret, + app.state.feishu_channel_stop_event, + app.state.feishu_channel_state, + ), + name="feishu-channel", + daemon=True, + ) + app.state.feishu_channel_thread.start() + print("feishu channel background thread started", flush=True) + + +async def _stop_feishu_channel(app: FastAPI) -> None: + stop_event = getattr(app.state, "feishu_channel_stop_event", None) + if stop_event is not None: + stop_event.set() + state = getattr(app.state, "feishu_channel_state", None) or {} + channel = state.get("channel") + if channel is not None: + await asyncio.to_thread(_stop_feishu_channel_from_lifespan, channel) + thread = getattr(app.state, "feishu_channel_thread", None) + if thread is not None: + await asyncio.to_thread(thread.join, 2) + if thread.is_alive(): + print( + "feishu channel background thread did not stop within 2s", + flush=True, + ) + + +def _configure_feishu_lifecycle( + app: FastAPI, + root_agent: BaseAgent, + short_term_memory: ShortTermMemory, +) -> None: + from veadk import Runner + + runner = Runner( + agent=root_agent, + app_name=getattr(root_agent, "name", "") or "agent", + short_term_memory=short_term_memory, + ) + original_lifespan = app.router.lifespan_context + + @asynccontextmanager + async def lifespan(fastapi_app: FastAPI): + async with original_lifespan(fastapi_app): + await _start_feishu_channel(fastapi_app, runner) + try: + yield + finally: + await _stop_feishu_channel(fastapi_app) + + app.router.lifespan_context = lifespan + + +def _add_introspection_routes( + app: FastAPI, + root_agent: BaseAgent, + display_names: Mapping[str, str], +) -> None: + @app.get("/ping") + def ping() -> dict[str, str]: + return {"status": "ok"} + + @app.get("/web/agent-info/{app_name}") + def agent_info(app_name: str) -> dict[str, Any]: + expected_name = str(getattr(root_agent, "name", "") or "") + if app_name != expected_name: + raise HTTPException(status_code=404, detail="unknown agent: " + app_name) + node = _agent_node(root_agent, display_names) + return { + **{key: node[key] for key in ("id", "name", "description", "type")}, + "model": node["model"], + "tools": node["tools"], + "subAgents": [ + _display_name( + str(getattr(child, "name", "") or ""), + display_names, + ) + for child in getattr(root_agent, "sub_agents", []) or [] + ], + "graph": node, + } + + @app.get("/web/agent-graph") + def agent_graph() -> dict[str, Any]: + node = _agent_node(root_agent, display_names) + return { + **{key: node[key] for key in ("id", "name", "description", "type")}, + "model": node["model"], + "tools": node["tools"], + "graph": node, + } + + +def _mount_webui(app: FastAPI) -> None: + import veadk + + webui_dir = Path(veadk.__file__).resolve().parent / "webui" + if not (webui_dir / "index.html").is_file(): + return + + if (webui_dir / "assets").is_dir(): + app.mount( + "/assets", + StaticFiles(directory=str(webui_dir / "assets")), + name="webui-assets", + ) + + @app.get("/") + @app.get("/webui") + @app.get("/webui/{path:path}") + def webui(path: str = "") -> FileResponse: + del path + return FileResponse(webui_dir / "index.html") + + +def _prioritize_platform_routes(app: FastAPI) -> None: + priority_paths = { + "/", + "/ping", + "/web/agent-info/{app_name}", + "/web/agent-graph", + "/assets", + "/webui", + "/webui/{path:path}", + } + priority_routes = [ + route + for route in app.router.routes + if getattr(route, "path", None) in priority_paths + ] + if priority_routes: + app.router.routes[:] = priority_routes + [ + route for route in app.router.routes if route not in priority_routes + ] + + +def create_agentkit_app( + root_agent: BaseAgent, + display_names: Mapping[str, str] | None = None, + *, + enable_feishu: bool = False, +) -> FastAPI: + """Create an AgentKit-compatible FastAPI app for ``root_agent``. + + The app includes AgentKit's conversation APIs, VeADK health and topology + endpoints, the bundled Web UI, local short-term memory fallback, and the + optional Feishu channel lifecycle. + + Args: + root_agent: Root ADK agent served by AgentKit. + display_names: User-facing names keyed by technical agent name. + enable_feishu: Whether to start the Feishu channel with credentials from + ``FEISHU_APP_ID`` and ``FEISHU_APP_SECRET``. + + Returns: + The configured FastAPI application. + """ + names = dict(display_names or {}) + short_term_memory = getattr(root_agent, "short_term_memory", None) + if short_term_memory is None: + short_term_memory = ShortTermMemory(backend="local") + + agent_server = AgentkitAgentServerApp( + agent=root_agent, + short_term_memory=short_term_memory, + ) + app = cast(FastAPI, agent_server.app) + setattr(app.state, _SERVER_STATE_KEY, agent_server) + + if enable_feishu: + _configure_feishu_lifecycle(app, root_agent, short_term_memory) + _add_introspection_routes(app, root_agent, names) + _mount_webui(app) + _prioritize_platform_routes(app) + return app + + +def run_agentkit_app( + app: FastAPI, + *, + host: str | None = None, + port: int | None = None, +) -> None: + """Run an app returned by :func:`create_agentkit_app`.""" + agent_server = getattr(app.state, _SERVER_STATE_KEY, None) + if agent_server is None: + raise ValueError("app was not created by create_agentkit_app") + resolved_host = host or os.getenv("HOST", "0.0.0.0") + resolved_port = port if port is not None else int(os.getenv("PORT", "8000")) + agent_server.run(host=resolved_host, port=resolved_port)