diff --git a/02-use-cases/smart_car_selector/.env.example b/02-use-cases/smart_car_selector/.env.example new file mode 100644 index 00000000..dfd3f059 --- /dev/null +++ b/02-use-cases/smart_car_selector/.env.example @@ -0,0 +1,49 @@ +AGENTKIT_TOOL_ID= +AGENTKIT_TOOL_REGION=cn-beijing +SKILL_SPACE_ID= +VEADK_APP_NAME=carselector_unified +VEADK_APP_DESCRIPTION= +LOGGING_LEVEL=INFO +DATABASE_POSTGRESQL_DATABASE= +DATABASE_POSTGRESQL_HOST= +DATABASE_POSTGRESQL_PASSWORD= +DATABASE_POSTGRESQL_USER= +DATABASE_TOS_BUCKET= +DATABASE_TOS_ENDPOINT=tos-cn-beijing.volces.com +DATABASE_TOS_REGION=cn-beijing +DATABASE_VIKING_COLLECTION_NAME= +DATABASE_VIKING_MEM_COLLECTION_NAME= +DATABASE_VIKING_PROJECT=default +DATABASE_VIKING_REGION=cn-beijing +MODEL_AGENT_API_KEY= +MODEL_AGENT_NAME= +MODEL_EMBEDDING_DIM=2560 +MODEL_EMBEDDING_NAME=doubao-embedding-text-240715 +MODEL_EVALUATE_ITEM=doubao-1-5-vision-pro-32k-250115 +EVENTS_COMPACTION_API_BASE=https://ark.cn-beijing.volces.com/api/v3/ +EVENTS_COMPACTION_API_KEY= +EVENTS_COMPACTION_COZELOOP_LABEL= +EVENTS_COMPACTION_COZELOOP_PROMPT_KEY= +EVENTS_COMPACTION_COZELOOP_TOKEN= +EVENTS_COMPACTION_COZELOOP_WORKSPACE_ID= +EVENTS_COMPACTION_INTERVAL=3 +EVENTS_COMPACTION_MODEL=openai/doubao-seed-1-6-lite-251015 +EVENTS_COMPACTION_OVERLAP_SIZE=1 +EVENTS_COMPACTION_PROMPT_FILE=compaction_prompt_template.txt +PROMPT_MANAGEMENT_COZELOOP_LABEL= +PROMPT_MANAGEMENT_COZELOOP_PROMPT_KEY= +PROMPT_MANAGEMENT_COZELOOP_TOKEN= +PROMPT_MANAGEMENT_COZELOOP_WORKSPACE_ID= +OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY= +OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT=http://apmplus-cn-beijing.volces.com:4317 +OBSERVABILITY_OPENTELEMETRY_APMPLUS_SERVICE_NAME= +OBSERVABILITY_OPENTELEMETRY_COZELOOP_API_KEY= +OBSERVABILITY_OPENTELEMETRY_COZELOOP_ENDPOINT=https://api.coze.cn/v1/loop/opentelemetry/v1/traces +OBSERVABILITY_OPENTELEMETRY_COZELOOP_SERVICE_NAME= +OBSERVABILITY_OPENTELEMETRY_TLS_ENDPOINT=https://tls-cn-beijing.volces.com:4318/v1/traces +OBSERVABILITY_OPENTELEMETRY_TLS_REGION=cn-beijing +PROMPT_MANAGEMENT_LOCAL_INSTRUCTION_FILE=instruction.md +VOLCENGINE_ACCESS_KEY= +VOLCENGINE_REGION=cn-beijing +VOLCENGINE_SECRET_KEY= +VEADK_BUILTIN_TOOLS_ENABLE=web_search,execute_skills,image_generate diff --git a/02-use-cases/smart_car_selector/ENV.md b/02-use-cases/smart_car_selector/ENV.md new file mode 100644 index 00000000..9e59949d --- /dev/null +++ b/02-use-cases/smart_car_selector/ENV.md @@ -0,0 +1,131 @@ +# 环境变量说明 + +本项目功能较多(模型、记忆、知识库、可观测、技能执行与文件上传等),因此需要较多环境变量。本文档说明每个变量的用途、示例取值,以及未配置时的行为。 + +## 使用方式 + +- **本地运行**:在终端里导出环境变量,或使用你自己的 `.env`/shell 配置方式(请不要提交密钥到仓库)。 +- **云端运行**:在 `agentkit.yaml` 的 `runtime_envs` 中配置(同样不要提交真实密钥到公开仓库)。 + +## 加载优先级(兼容本地调试) + +VeADK 会按如下优先级读取配置(优先级由高到低): + +1. 系统环境变量 +2. `.env` 文件 +3. `config.yaml` 文件 + +`.env` 的查找通常与**进程启动时的工作目录(cwd)**有关,因此有两种常见放置方式: + +- 放在 `agent/.env`(推荐):配合默认启动命令 `cd agent && python3 agentkit-agent.py`,VeADK 会在当前目录读取 `.env`。 +- 放在仓库根目录 `.env`:需要从根目录启动(例如 `python3 agent/agentkit-agent.py`),否则 VeADK 在 `agent/` 下启动时不会自动读取到根目录的 `.env`。 + +## 最小可用(本地) + +以下变量缺失会导致模型无法正常调用: + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `MODEL_AGENT_API_KEY` | 大模型调用的 API Key(VeADK/Agent 会用它访问模型服务) | `MODEL_AGENT_API_KEY="YOUR_API_KEY"` | 模型请求将失败(鉴权失败或无 Key) | + +获取方式:在火山引擎控制台创建/查看你的模型服务 API Key,并将其以环境变量或 `.env` 的方式注入(不要提交到仓库)。 + +## 应用基础信息 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `VEADK_APP_NAME` | 应用名(同时影响记忆/知识库的 app_name 命名空间) | `carselector_unified` | 默认 `carselector_unified` | +| `VEADK_AGENT_NAME` | Agent 名称 | `carselector_unified` | 默认等于 `VEADK_APP_NAME` | +| `VEADK_APP_DESCRIPTION` | 应用描述 | `智能选车助手(Unified)` | 使用内置默认描述 | +| `VEADK_ENABLE_RESPONSES` | 是否开启 responses 相关能力(布尔) | `true` / `false` | 默认 `false` | + +## 工具开关(Built-in Tools) + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `VEADK_BUILTIN_TOOLS_ENABLE` | 内置工具白名单(逗号分隔) | `web_search,execute_skills,image_generate` | 为空时:加载代码中可导入的全部内置工具 | + +## Prompt / 指令 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `PROMPT_MANAGEMENT_LOCAL_INSTRUCTION_FILE` | 本地指令文件路径(相对路径相对 `agent/`) | `instruction.md` | 默认 `instruction.md` | +| `PROMPT_MANAGEMENT_COZELOOP_PROMPT_KEY` | Cozeloop Prompt Key(启用远端 Prompt 管理) | `carselector_unified_sysprompt` | 为空:不启用 Cozeloop PromptManager(使用本地 instruction) | +| `PROMPT_MANAGEMENT_COZELOOP_WORKSPACE_ID` | Cozeloop Workspace ID | `YOUR_WORKSPACE_ID` | 为空:不启用 Cozeloop PromptManager | +| `PROMPT_MANAGEMENT_COZELOOP_TOKEN` | Cozeloop Token | `REPLACE_ME` | 为空:不启用 Cozeloop PromptManager | +| `PROMPT_MANAGEMENT_COZELOOP_LABEL` | Cozeloop Label(环境/版本标签) | `beta` | 默认 `beta` | + +## 会话与记忆(Memory) + +短期记忆在本地默认使用 `local`,如果配置了 PostgreSQL 则切换为 `postgresql`;长期记忆与知识库则按需启用。 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `DATABASE_POSTGRESQL_HOST` | PostgreSQL Host(短期记忆启用条件) | `127.0.0.1` | 为空:短期记忆使用本地 `local` 后端 | +| `DATABASE_POSTGRESQL_DATABASE` | PostgreSQL DB 名 | `carselector_mem` | 为空:短期记忆使用本地 `local` 后端 | +| `DATABASE_POSTGRESQL_USER` | PostgreSQL 用户名 | `postgres` | 为空:短期记忆使用本地 `local` 后端 | +| `DATABASE_POSTGRESQL_PASSWORD` | PostgreSQL 密码 | `YOUR_PASSWORD` | 为空:短期记忆使用本地 `local` 后端 | +| `DATABASE_VIKING_MEM_COLLECTION_NAME` | Viking 记忆集合名(长期记忆) | `carselector_shared_memory` | 为空:不启用长期记忆,且不会自动保存会话 | +| `DATABASE_VIKING_COLLECTION_NAME` | Viking 知识库集合名 | `carselector_kb` | 为空:不启用知识库 | +| `DATABASE_VIKING_PROJECT` | Viking Project | `default` | 由后端 SDK 自行处理;为空可能使用默认值 | +| `DATABASE_VIKING_REGION` | Viking Region | `cn-beijing` | 由后端 SDK 自行处理;为空可能使用默认值 | + +## 文件上传(TOS) + +本项目会在需要生成图片/表格等文件时上传到 TOS 并返回可下载链接(如果未配置则会回退为“无法上传”的错误信息)。 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `DATABASE_TOS_BUCKET` | TOS Bucket 名 | `your-bucket` | 为空:上传功能不可用(会返回缺少 bucket 的错误) | +| `DATABASE_TOS_ENDPOINT` | TOS Endpoint | `tos-cn-beijing.volces.com` | 默认 `tos-cn-beijing.volces.com` | +| `DATABASE_TOS_REGION` | TOS Region | `cn-beijing` | 默认 `cn-beijing` | +| `VOLCENGINE_ACCESS_KEY` | 火山 AK(用于签名上传/下载) | `AKxxx` | 为空:无法初始化 TOS 客户端,上传不可用 | +| `VOLCENGINE_SECRET_KEY` | 火山 SK(用于签名上传/下载) | `xxxx` | 为空:无法初始化 TOS 客户端,上传不可用 | +| `VOLCENGINE_REGION` | 火山 Region | `cn-beijing` | 由后端 SDK 自行处理;为空可能使用默认值 | + +## 事件压缩(Events Compaction) + +`agentkit-agent.py` 会给 A2A App 配置事件压缩,未配置时使用默认模型与参数。 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `EVENTS_COMPACTION_MODEL` | 用于摘要/压缩事件的模型标识 | `openai/doubao-seed-1-6-lite-251015` | 使用默认值 | +| `EVENTS_COMPACTION_API_BASE` | 事件压缩模型的 API Base | `https://ark.cn-beijing.volces.com/api/v3/` | 使用默认值 | +| `EVENTS_COMPACTION_API_KEY` | 事件压缩模型的 API Key | `YOUR_API_KEY` | 为空:回退使用 `MODEL_AGENT_API_KEY` | +| `EVENTS_COMPACTION_INTERVAL` | 压缩触发间隔(整数) | `3` | 默认 `3` | +| `EVENTS_COMPACTION_OVERLAP_SIZE` | 压缩重叠窗口(整数) | `1` | 默认 `1` | +| `EVENTS_COMPACTION_PROMPT_FILE` | 压缩 prompt 模板文件名/路径(按运行环境约定) | `compaction_prompt_template.txt` | 由运行环境/实现自行处理 | +| `EVENTS_COMPACTION_COZELOOP_PROMPT_KEY` | Cozeloop 下的 compaction prompt key | `carselector_events_compaction_prompt` | 为空:不使用 Cozeloop compaction prompt | +| `EVENTS_COMPACTION_COZELOOP_WORKSPACE_ID` | Cozeloop Workspace ID | `YOUR_WORKSPACE_ID` | 为空:不使用 Cozeloop compaction prompt | +| `EVENTS_COMPACTION_COZELOOP_TOKEN` | Cozeloop Token | `REPLACE_ME` | 为空:不使用 Cozeloop compaction prompt | +| `EVENTS_COMPACTION_COZELOOP_LABEL` | Cozeloop Label | `beta` | 由实现自行处理 | + +## 可观测(Observability) + +只要对应 endpoint 未配置,就会自动跳过 tracer/exporter 初始化。 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT` | APMPlus OTLP Endpoint | `http://apmplus:4317` | 为空:不启用 APMPlus exporter | +| `OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY` | APMPlus API Key | `YOUR_APMPLUS_KEY` | 为空:不启用 APMPlus exporter | +| `OBSERVABILITY_OPENTELEMETRY_APMPLUS_SERVICE_NAME` | APMPlus 服务名 | `carselector_unified` | 由 exporter/实现自行处理 | +| `OBSERVABILITY_OPENTELEMETRY_COZELOOP_ENDPOINT` | Cozeloop OTLP Endpoint | `https://api.coze.cn/.../traces` | 为空:不启用 Cozeloop exporter | +| `OBSERVABILITY_OPENTELEMETRY_COZELOOP_API_KEY` | Cozeloop API Key/Token | `REPLACE_ME` | 为空:不启用 Cozeloop exporter | +| `OBSERVABILITY_OPENTELEMETRY_COZELOOP_SERVICE_NAME` | Cozeloop 服务名 | `YOUR_WORKSPACE_ID` | 由 exporter/实现自行处理 | +| `OBSERVABILITY_OPENTELEMETRY_TLS_ENDPOINT` | TLS OTLP Endpoint | `https://tls.../traces` | 为空:不启用 TLS exporter | +| `OBSERVABILITY_OPENTELEMETRY_TLS_REGION` | TLS Region | `cn-beijing` | 由 exporter/实现自行处理 | + +## AgentKit(云端/应用场景) + +这些变量主要用于云端运行时的“工具/技能空间”定位与配置;本地运行通常不需要全部配置。 + +| 变量 | 含义 | 示例 | 为空时行为 | +|---|---|---|---| +| `AGENTKIT_TOOL_ID` | AgentKit 工具 ID | `t_xxx` | 由云端部署平台决定;为空可能导致工具不可用 | +| `AGENTKIT_TOOL_REGION` | AgentKit 工具 Region | `cn-beijing` | 由云端部署平台决定 | +| `SKILL_SPACE_ID` | Skill Space ID | `ss_xxx` | 由云端部署平台决定;为空可能导致 skills 不可用 | + +## 安全建议(强烈) + +- 不要把任何 `*_API_KEY`、`*_TOKEN`、`*_PASSWORD`、`VOLCENGINE_*` 这类敏感信息提交到仓库。 +- 推荐在本地使用环境变量或密钥管理系统(KMS),在 CI/CD 与云端部署通过安全注入方式提供密钥。 diff --git a/02-use-cases/smart_car_selector/LICENSE b/02-use-cases/smart_car_selector/LICENSE new file mode 100644 index 00000000..eac7a78a --- /dev/null +++ b/02-use-cases/smart_car_selector/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 + + 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/02-use-cases/smart_car_selector/README.md b/02-use-cases/smart_car_selector/README.md new file mode 100644 index 00000000..4a0cce8e --- /dev/null +++ b/02-use-cases/smart_car_selector/README.md @@ -0,0 +1,68 @@ +# AgentKit-小懂车 (Smart Car Selector) + +**AgentKit-小懂车** 是一个基于 [AgentKit](https://github.com/bytedance/agentkit) 构建的“单体 Unified Agent”购车咨询示例:用一个 Agent 覆盖用户真实选车旅程——求推荐、参数对比、口碑避坑、价格/金融算账、预约试驾与导航、选中后生成海报提供情绪价值。 + +项目主线采用 **Single Agent + Tools/Skills**:在不引入多智能体编排复杂度的前提下,保留可复核的确定性交付(Excel 表、可视化图片、地图路线等)。 + +## ✨ 核心亮点 + +- **真实选车场景**:推荐 → 参数表 → 口碑 → 金融表 → 试驾导航 → 海报,一条链路走完。 +- **可复核交付**:参数对比表、金融分析表均可下载,可复用公式并支持用户改参数重算。 +- **事实核验优先**:涉及参数/政策/口碑等事实点时优先 `web_search` 交叉核验,避免拍脑袋。 +- **交付执行能力(可选)**:如启用 `mcp_router` 可做门店/路线/导航;未启用时也会输出门店筛选方法与到店清单,保证体验可用。 +- **情绪价值补全**:选中车型后可用 `image_generate` 生成可分享海报与文案。 +- **趣味彩蛋**:用户纠结难以拍板时,用掷骰子做“玄学推荐”并给理性兜底问题。 + +## 📂 项目结构 + +```text +. +├── agent.py # 核心 Agent(Unified) +├── agentkit-agent.py # A2A 服务入口 +├── agentkit.yaml # AgentKit 配置(已脱敏) +├── instruction.md # 系统提示词(包含完整选车旅程) +├── utils.py # 工具辅助(上传/清理输出等) +├── requirements.txt +├── pyproject.toml +├── project.toml +├── ENV.md +├── img/ +└── skills/ + +skills/ +└── dice-roller/ # 掷骰子彩蛋 Skill +``` + +## 🚀 本地启动 + +```bash +python3 -m pip install -r requirements.txt +python3 agentkit-agent.py +python3 -c "import requests; print(requests.get('http://127.0.0.1:8000/ping', timeout=5).text)" +``` + +环境变量较多,详见 [ENV.md](ENV.md)(建议复制 `.env.example` 为 `.env` 并填写必要项;不要把真实密钥提交到仓库)。 + +## 🧩 架构图 + +![场景架构图](img/process_smart_car_selector.jpg) + +![Technical Architecture](img/archtecture_smart_car_selector.jpg) + +## 🧭 典型用户旅程(你可以这样体验) + +1. **求推荐**:给出预算/城市/用车场景,获取 2-3 款候选与取舍建议。 +2. **对比参数**:说“对比参数/出参数表”,拿到可下载 Excel 参数对比表。 +3. **对比口碑**:说“这车通病/能买吗”,获取真实车主反馈与避坑点。 +4. **对比价格**:说“落地价/月供/还款表”,拿到可下载金融分析 Excel(含可调参数)。 +5. **预约试驾**:说“帮我找店/导航/预约试驾”,输出门店与路线,并附预约话术与到店清单。 +6. **生成海报**:选中车型后说“做个海报/朋友圈文案”,生成可分享海报。 + +## 📦 标准文件 + +- README.md:中文说明。 +- README_en.md:英文说明。 +- requirements.txt:依赖列表(本地运行)。 +- pyproject.toml:本地 Python 工具链/打包元数据(可选)。 +- project.toml:应用元数据(用于应用广场/发布场景)。 +- LICENSE:默认 Apache-2.0 协议。 diff --git a/02-use-cases/smart_car_selector/README_en.md b/02-use-cases/smart_car_selector/README_en.md new file mode 100644 index 00000000..d6de31cd --- /dev/null +++ b/02-use-cases/smart_car_selector/README_en.md @@ -0,0 +1,67 @@ +# AgentKit Smart Car Selector + +**AgentKit Smart Car Selector** is a “single unified agent” sample built on top of [AgentKit](https://github.com/bytedance/agentkit). It simulates a real car-buying journey end-to-end: recommendations, spec comparison (downloadable table), reputation checks, price/finance analysis (downloadable table), test-drive planning (store & navigation), and a shareable poster after the final pick. + +The main design is **Single Agent + Tools/Skills**: keep the system simple to deploy while still delivering deterministic, verifiable artifacts (Excel sheets, images, map routes). + +## Highlights + +- Real user journey: Recommend → Specs → Reputation → Finance → Test Drive → Poster +- Verifiable outputs: downloadable Excel sheets with reusable formulas +- Fact-check first: use `web_search` for specs/policy/reputation when needed +- Delivery execution (optional): if `mcp_router` is enabled, use it for POI / route / navigation; otherwise provide store-search guidance and an actionable checklist. +- Emotional value: `image_generate` for shareable posters and copy +- Fun fallback: dice-based “mystic pick” when users can’t decide + +## Project Layout + +```text +. +├── agent.py # Unified agent +├── agentkit-agent.py # A2A server entry +├── agentkit.yaml # AgentKit config (redacted) +├── instruction.md # System prompt (full car-buying journey) +├── utils.py +├── requirements.txt +├── pyproject.toml +├── project.toml +├── ENV.md +├── img/ +└── skills/ + +skills/ +└── dice-roller/ # dice “mystic pick” skill +``` + +## Quick Start (Local) + +```bash +python3 -m pip install -r requirements.txt +python3 agentkit-agent.py +python3 -c "import requests; print(requests.get('http://127.0.0.1:8000/ping', timeout=5).text)" +``` + +This project uses many environment variables. See [ENV.md](ENV.md). Copy `.env.example` to `.env` and fill required values (do not commit secrets). + +## Architecture Diagrams + +![Scene Architecture](img/process_smart_car_selector.jpg) + +![Technical Architecture](img/archtecture_smart_car_selector.jpg) + +## Typical User Flow + +1. Ask for recommendations with your budget/city/use-case. +2. Ask for a spec comparison table (“export an Excel table”). +3. Ask for reputation checks (“common issues / owner feedback”). +4. Ask for price & finance analysis (“OTD / monthly payment / amortization table”). +5. Ask for test-drive planning (“find stores / navigation / appointment script”). +6. Ask for a shareable poster after you pick a model. + +## Required Files (Marketplace / Packaging) + +- `README.md` / `README_en.md`: project documentation (Chinese / English). +- `requirements.txt`: dependency list for local development. +- `pyproject.toml`: optional local packaging metadata for Python tools. +- `project.toml`: application marketplace metadata. +- `LICENSE`: license file (Apache-2.0 by default). diff --git a/02-use-cases/smart_car_selector/agent.py b/02-use-cases/smart_car_selector/agent.py new file mode 100644 index 00000000..b0c166c2 --- /dev/null +++ b/02-use-cases/smart_car_selector/agent.py @@ -0,0 +1,66 @@ +import os +import logging +from pathlib import Path + +from veadk import Agent + +from utils import ( + init_veadk_builtin_tools, + init_short_term_memory, + init_long_term_memory, + init_knowledge_base, + init_prompt_manager, + init_observability, + load_local_instruction, + callback_cleanup_model_output, + callback_cleanup_tool_output, +) + +logger = logging.getLogger(__name__) + +app_name = os.getenv("VEADK_APP_NAME", "carselector_unified") +agent_name = os.getenv("VEADK_AGENT_NAME", app_name) +description = os.getenv("VEADK_APP_DESCRIPTION", "智能选车助手(Unified)") + +APP_NAME = app_name +DESCRIPTION = description + +instruction_file = os.getenv("PROMPT_MANAGEMENT_LOCAL_INSTRUCTION_FILE", "instruction.md").strip() +instruction_path = Path(instruction_file) +if not instruction_path.is_absolute(): + instruction_path = Path(__file__).parent / instruction_path + +short_term = init_short_term_memory(app_name) +long_term = init_long_term_memory(app_name) +knowledge = init_knowledge_base(app_name) +prompt_manager = init_prompt_manager() +tracers = init_observability() +instruction = load_local_instruction(instruction_path) + +tools = init_veadk_builtin_tools() + +enable_responses = os.getenv("VEADK_ENABLE_RESPONSES", "false").strip().lower() in {"1", "true", "yes"} + +agent_kwargs = { + "name": agent_name, + "description": description, + "model_api_key": os.getenv("MODEL_AGENT_API_KEY", ""), + "tools": tools, + "after_model_callback": callback_cleanup_model_output, + "after_tool_callback": callback_cleanup_tool_output, + "short_term_memory": short_term, + "auto_save_session": bool(long_term), + "enable_responses": enable_responses, +} + +optional_kwargs = { + "knowledgebase": knowledge, + "long_term_memory": long_term, + "instruction": instruction, + "prompt_manager": prompt_manager, + "tracers": tracers or None, +} +agent_kwargs.update({k: v for k, v in optional_kwargs.items() if v}) + +car_unified_agent = Agent(**agent_kwargs) +root_agent = car_unified_agent diff --git a/02-use-cases/smart_car_selector/agentkit-agent.py b/02-use-cases/smart_car_selector/agentkit-agent.py new file mode 100644 index 00000000..45e13010 --- /dev/null +++ b/02-use-cases/smart_car_selector/agentkit-agent.py @@ -0,0 +1,80 @@ +import logging +import os + +from agent import car_unified_agent, APP_NAME, DESCRIPTION + +from veadk import Runner +from google.adk.apps.app import App, EventsCompactionConfig +from google.adk.apps.llm_event_summarizer import LlmEventSummarizer +from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor +from google.adk.models.lite_llm import LiteLlm +from agentkit.apps import AgentkitA2aApp +from a2a.types import AgentCard, AgentProvider, AgentSkill, AgentCapabilities + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +a2a_app = AgentkitA2aApp() + +session_service = ( + car_unified_agent.short_term_memory.session_service if car_unified_agent.short_term_memory else None +) +runner = Runner(agent=car_unified_agent, session_service=session_service) +runner.app = App( + name=APP_NAME, + root_agent=car_unified_agent, + events_compaction_config=EventsCompactionConfig( + summarizer=LlmEventSummarizer( + llm=LiteLlm( + model=os.getenv("EVENTS_COMPACTION_MODEL", "openai/doubao-seed-1-6-lite-251015"), + api_base=os.getenv("EVENTS_COMPACTION_API_BASE", "https://ark.cn-beijing.volces.com/api/v3/"), + api_key=os.getenv("EVENTS_COMPACTION_API_KEY", os.getenv("MODEL_AGENT_API_KEY", "")), + ), + prompt_template=None, + ), + compaction_interval=int(os.getenv("EVENTS_COMPACTION_INTERVAL", "3")), + overlap_size=int(os.getenv("EVENTS_COMPACTION_OVERLAP_SIZE", "1")), + ), +) + + +@a2a_app.agent_executor(runner=runner) +class CarUnifiedAgentExecutor(A2aAgentExecutor): + async def run(self, *args, **kwargs): + try: + return await super().run(*args, **kwargs) + except Exception as e: + logger.exception("Error executing agent request") + raise e + + +@a2a_app.ping +def ping() -> str: + return "pong!" + + +if __name__ == "__main__": + agent_card = AgentCard( + capabilities=AgentCapabilities(streaming=True), + description=DESCRIPTION, + name=APP_NAME, + default_input_modes=["text"], + default_output_modes=["text"], + provider=AgentProvider(organization="volcengine", url=""), + skills=[ + AgentSkill( + id="car_unified", + name="car_unified", + description="一体化选车+算账+交付", + tags=["chat", "car", "unified"], + ) + ], + url="http://0.0.0.0:8000", + version="1.0.0", + ) + + a2a_app.run( + agent_card=agent_card, + host="0.0.0.0", + port=8000, + ) diff --git a/02-use-cases/smart_car_selector/img/archtecture_smart_car_selector.jpg b/02-use-cases/smart_car_selector/img/archtecture_smart_car_selector.jpg new file mode 100644 index 00000000..54c7680c Binary files /dev/null and b/02-use-cases/smart_car_selector/img/archtecture_smart_car_selector.jpg differ diff --git a/02-use-cases/smart_car_selector/img/process_smart_car_selector.jpg b/02-use-cases/smart_car_selector/img/process_smart_car_selector.jpg new file mode 100644 index 00000000..7c2eb3ac Binary files /dev/null and b/02-use-cases/smart_car_selector/img/process_smart_car_selector.jpg differ diff --git a/02-use-cases/smart_car_selector/instruction.md b/02-use-cases/smart_car_selector/instruction.md new file mode 100644 index 00000000..7209139c --- /dev/null +++ b/02-use-cases/smart_car_selector/instruction.md @@ -0,0 +1,103 @@ +# 智能选车助手(Unified) + +你是“智能选车私人顾问”,以单体 Agent 的方式提供从「选车咨询」到「算账」再到「试驾落地」的一站式服务。你需要像一个“购车参谋团”那样工作:在脑中切换不同专家视角,但最终只对外输出一个统一、可执行、可复核的结论。 + +## 角色与能力 +你同时具备三种工作模式(在同一回复中可组合使用): +- **全能导购(Consultant)**:需求分析、车型推荐、参数查询、竞品对比、口碑避坑。 +- **成本金融(Finance)**:落地价、月供、利率口径校验、总利息、TCO 预估、Excel 报表。 +- **交付执行(Delivery)**:门店/路线/导航、试驾流程建议、物料生成(海报/图表)、彩蛋互动(掷骰子玄学推荐)。 + +## 开场白策略(MANDATORY) +当用户首次进入或说“你好/在吗/你能做什么”等时,严禁只输出一句话。必须用 Markdown 做结构化自我介绍,展示你能做的事情,并用 1 句引导用户给出关键信息(预算/偏好/城市)。 + +## 总体流程(STRICT) +1. **识别意图**:选车推荐 / 口碑核验 / 价格与金融 / 交付与工具(地图、图表、海报、彩蛋)。 +2. **一次性追问**:信息不足时只做 1 轮“最小关键追问”,避免连环拷问;用户不想回答就用行业默认值继续。 +3. **工具化与可复核**: + - 参数/价格/政策/口碑等事实性信息:优先 `web_search` 核验;无法核验时必须显式标注不确定性与假设范围。 + - 涉及计算/制表:必须 `execute_skills` 产出结构化结果或表格,再用自然语言解释。 + - 门店/路线/通勤:优先 `mcp_router`(如已启用);若不可用则输出可执行的门店筛选方法与到店建议,并提示用户用地图 App 生成导航。 + - 海报/可视化:优先 `image_generate`;绘图类用 `execute_skills` 跑脚本生成图片。 +4. **结果审计**:你需要自己检查输出是否与用户预算、需求一致;并指出关键 trade-off(空间/续航/智驾/成本/保值)。 +5. **旅程引导**:每次回复末尾必须追加“下一步建议”(见下文模板)。 + +## 场景旅程(真实用户选车链路) +当用户从“纠结不知道买啥”进入时,优先用下面的链路组织对话与交付(按需跳过某些环节,但要明确说明跳过原因): +1. **求推荐(候选池)**:给 2-3 个候选(含适用人群与 trade-off)。涉及参数/政策/口碑/价格等事实点时用 `web_search` 交叉核验。 +2. **对比参数(可下载表)**:当用户说“对比参数/参数表/横向对照”时,用 `execute_skills` 生成 Excel 参数对比表(可复用公式、可编辑)。 +3. **对比口碑(避坑)**:当用户说“口碑/通病/值不值/能买吗”时,用 `web_search` 汇总真实车主常见优缺点与风险点,并给出适用人群建议。 +4. **对比价格(金融分析表)**:当用户说“落地价/月供/利率/总利息/还款表/金融方案”时,用 `execute_skills` 生成金融分析 Excel(含落地价拆分、月供与总成本、可调参数)。 +5. **预约试驾(自动化预约 + 导航)**:当用户说“预约试驾/去店里/门店/路线/导航”时: + - 若已启用 `mcp_router`:给出门店 POI、路线与导航。 + - 若未启用 `mcp_router`:给出门店筛选关键词(品牌/城市/“4S 店”)、建议的到店信息清单与可复制预约话术,并提示用户在地图 App 里选择最近门店并一键导航。 +6. **选中后海报(情绪价值)**:当用户确定车型或说“发个海报/朋友圈文案/仪式感”时,用 `image_generate` 生成海报并配 1 段可分享文案。 + +## 追问优先级(缺什么问什么) +- 选车:预算区间、用车城市/路况、家庭人数与空间、动力偏好(油/混/纯电)、通勤里程、是否看重智驾/保值。 +- 口碑:关注点(续航/异响/车机/底盘/售后)、是否介意小毛病、计划用车年限。 +- 算账:车型配置与成交价(或指导价)、首付比例/金额、期数、利率口径(年化/APR/费率)、税险上牌是否允许估算。 +- 交付:城市或定位、门店偏好(品牌/距离)、时间窗口、是否愿意提供联系方式(不强迫)。 + +## 工具与 Skills 使用规范 +### web_search(事实核验) +- 用于核验参数、价格、政策、口碑;不要凭空报精确数值。 +- 查口碑时优先检索真实车主反馈与缺点关键词(例如“某车型 缺点/通病/车主评价”)。 + +### execute_skills(计算/表格/脚本) +- 金融计算、Excel 表、绘图脚本必须走 `execute_skills`。 +- Excel 文件建议直接使用预置 `xlsx` 能力生成(等额本息可用 `PMT / IPMT / PPMT` 等公式做还款表)。 +- **文件结果处理(CRITICAL)**:如果工具返回的是可下载链接(如 TOS URL),必须用 Markdown 链接形式输出;严禁输出本地路径(/tmp、/sandbox 等)。 + +### mcp_router(地图/门店/路线) +- 若已启用:涉及 POI、距离、路线、导航、试驾门店查询与到店路径时优先使用 `mcp_router`。 +- 若未启用:输出明确的门店筛选方法、路线规划建议与到店清单,并提示用户用地图 App 完成导航。 + +### image_generate(海报) +- 生成海报时优先 `image_generate`;输出必须用 Markdown 图片语法展示图片链接。 + +## 金融与成本输出规范(Finance Mode) +### 核心原则 +- 不完整数据允许用行业平均值估算,并明确标注“预估/假设”。 +- 销售口径“费率”必须转换为真实年化 APR,并提示用户关注隐性费用。 + +### Excel 报表(当用户要“还款表/Excel/明细”时必须遵守) +- 购置税:按能源类型与最新政策处理;燃油车可用 `裸车价 / 1.13 * 税率` 估算。 +- 月供:默认等额本息,输出每期本金/利息/剩余本金。 +- 汇总:清晰给出“首次付款总额”(首付+税费+保险+上牌等)与“总支付金额”。 +- 多车型对比表:当用户同时给出多款车型(或要求“对比/横向对照/参数表”)时,额外生成一个 Excel 参数对比表(同一文件中新增 sheet),按“车型一列/指标多行”或“指标一列/车型多列”组织;至少包含:指导价/成交价、首付比例与金额、期数、APR、月供、总利息、购置税/上牌/保险、首次付款总额、总支付金额;并确保关键指标用公式计算可复用(用户改参数可自动更新)。 + +## 交付与物料输出规范(Delivery Mode) +- 画图任务:用 `execute_skills` 生成并保存图片文件,最终用 Markdown 图片语法展示链接,并附 1-3 句解读。 +- 海报任务:用 `image_generate` 生成后展示图片,并配一段简短、可分享的文案。 +- 彩蛋(掷骰子玄学推荐):当用户明确表示“纠结/随缘/不知道选哪台/帮我拍板”时,用 `execute_skills` 掷骰子做随机选择;输出需包含“点数 + 玄学推荐(给出 1 个明确结论)+ 理性兜底(再列 2-3 个关键确认问题)+ 下一步指引”结构;严禁暗示“中奖/奖品/福利”。 + +## 调试模式(Developer Override) +当用户指令中包含“调试/测试/运行工作流”或明确指定工具操作时: +- 跳过冗长追问与合规流程提示,直接以工具调用准确性为最高目标。 +- 仍需把工具输出以可执行的形式返回(链接/图片/路线/结果摘要)。 + +## 输出模板(统一规范) +### 推荐/对比(Consultant) +1. 结论:2-3 个候选(按匹配度排序)。 +2. Why Buy:每款 2-3 条核心理由。 +3. Watch Out ⚠️:每款 1-3 条真实槽点与适用人群提醒。 +4. 选择建议:给出明确建议与“如果你更看重 X 就选 A”的分流。 + +### 算账(Finance) +1. 关键数字:落地价 / 首付 / 月供 / 总利息 / 首次付款总额。 +2. 假设:税险上牌/利率口径/补贴政策等。 +3. 可调参数:首付比例、期数、利率、保险预算。 + +### 结果交付(Delivery) +1. 可执行结果:路线/门店/图片/文件链接。 +2. 解释与注意事项:1-3 句。 + +## 旅程引导(MANDATORY) +每次回复末尾必须追加如下格式,引导用户下一步行动: +```markdown +--- +> 💡 下一步建议: +> 1. … +> 2. … +``` diff --git a/02-use-cases/smart_car_selector/project.toml b/02-use-cases/smart_car_selector/project.toml new file mode 100644 index 00000000..47c1f17a --- /dev/null +++ b/02-use-cases/smart_car_selector/project.toml @@ -0,0 +1,15 @@ +[app] +name = "AgentKit-小懂车" +name_en = "Smart Car Selector" +id = "smart-car-selector" +version = "0.1.0" +description = "一个基于 AgentKit 的单体购车咨询助手示例(选车 + 算账 + 交付)。" +entry = "agentkit-agent.py" +readme = "README.md" +readme_en = "README_en.md" +license = "Apache-2.0" +tags = ["car", "assistant", "agentkit", "unified"] + +[runtime] +language = "python" +python = ">=3.12" diff --git a/02-use-cases/smart_car_selector/pyproject.toml b/02-use-cases/smart_car_selector/pyproject.toml new file mode 100644 index 00000000..a9dd24f7 --- /dev/null +++ b/02-use-cases/smart_car_selector/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "smart-car-selector" +version = "0.1.0" +description = "Unified car selection assistant built with AgentKit" +readme = "README.md" +requires-python = ">=3.12" +license = { file = "LICENSE" } +dependencies = [ + "google-adk", + "agentkit-sdk-python>=0.4.2", + "veadk-python", + "requests", + "uvicorn", + "cozeloop", +] diff --git a/02-use-cases/smart_car_selector/requirements.txt b/02-use-cases/smart_car_selector/requirements.txt new file mode 100644 index 00000000..4f368093 --- /dev/null +++ b/02-use-cases/smart_car_selector/requirements.txt @@ -0,0 +1,6 @@ +google-adk +agentkit-sdk-python +veadk-python +requests +uvicorn +cozeloop diff --git a/02-use-cases/smart_car_selector/skills/dice-roller/SKILL.md b/02-use-cases/smart_car_selector/skills/dice-roller/SKILL.md new file mode 100644 index 00000000..32752462 --- /dev/null +++ b/02-use-cases/smart_car_selector/skills/dice-roller/SKILL.md @@ -0,0 +1,41 @@ +--- +name: dice-roller +description: 一个简单的掷骰子工具。用于生成随机数,或在用户纠结时做“玄学推荐”。 +version: 1.0.0 +entrypoint: python roll.py +args: + sides: + type: int + description: 骰子的面数(默认为 6)。 + required: false + count: + type: int + description: 掷骰子的数量(默认为 1)。 + required: false +output: + type: json + description: 返回掷骰子的结果,包括每次投掷的点数和总和。 +--- + +# 掷骰子工具 (Dice Roller) + +这是一个简单的掷骰子工具。 + +## 功能 +- 支持自定义骰子面数(默认为 6 面)。 +- 支持自定义掷骰子数量(默认为 1 个)。 +- 返回每次投掷的结果和总和。 + +## 参数说明 +- `sides`: 骰子面数 (Integer) +- `count`: 骰子数量 (Integer) + +## 返回示例 +```json +{ + "status": "success", + "results": [3, 5], + "total": 8, + "details": "Rolled 2d6" +} +``` diff --git a/02-use-cases/smart_car_selector/skills/dice-roller/requirements.txt b/02-use-cases/smart_car_selector/skills/dice-roller/requirements.txt new file mode 100644 index 00000000..e69de29b diff --git a/02-use-cases/smart_car_selector/skills/dice-roller/roll.py b/02-use-cases/smart_car_selector/skills/dice-roller/roll.py new file mode 100644 index 00000000..1e3bee54 --- /dev/null +++ b/02-use-cases/smart_car_selector/skills/dice-roller/roll.py @@ -0,0 +1,34 @@ +import argparse +import random +import json +import sys + +def main(): + parser = argparse.ArgumentParser(description="Roll some dice.") + parser.add_argument("--sides", type=int, default=6, help="Number of sides on the die") + parser.add_argument("--count", type=int, default=1, help="Number of dice to roll") + args = parser.parse_args() + + try: + if args.sides < 1: + raise ValueError("Sides must be at least 1") + if args.count < 1: + raise ValueError("Count must be at least 1") + + results = [random.randint(1, args.sides) for _ in range(args.count)] + total = sum(results) + + output = { + "status": "success", + "results": results, + "total": total, + "details": f"Rolled {args.count}d{args.sides}" + } + print(json.dumps(output)) + + except Exception as e: + print(json.dumps({"status": "error", "message": str(e)})) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/02-use-cases/smart_car_selector/utils.py b/02-use-cases/smart_car_selector/utils.py new file mode 100644 index 00000000..aa20c9d9 --- /dev/null +++ b/02-use-cases/smart_car_selector/utils.py @@ -0,0 +1,238 @@ +import os +import uuid +import base64 +import logging +import re +import importlib +from pathlib import Path +from typing import Any, Optional + +logger = logging.getLogger(__name__) + + +def get_tos_client(): + ak = os.getenv("VOLCENGINE_ACCESS_KEY") + sk = os.getenv("VOLCENGINE_SECRET_KEY") + endpoint = os.getenv("DATABASE_TOS_ENDPOINT", "tos-cn-beijing.volces.com") + region = os.getenv("DATABASE_TOS_REGION", "cn-beijing") + if not ak or not sk: + return None + try: + import tos + except Exception: + return None + return tos.TosClientV2(ak, sk, endpoint, region) + + +def upload_bytes_to_tos(data_bytes: bytes, extension: str = ".png"): + bucket = os.getenv("DATABASE_TOS_BUCKET") + if not bucket: + return None, "Missing DATABASE_TOS_BUCKET env var" + + client = get_tos_client() + if not client: + return None, "Failed to initialize TOS client" + + key = f"agent_uploads/{uuid.uuid4()}{extension}" + try: + client.put_object(bucket, key, content=data_bytes) + url = client.pre_signed_url( + http_method="GET", + bucket=bucket, + key=key, + expires=3600, + ) + return url, None + except Exception as e: + return None, f"Failed to upload to TOS: {str(e)}" + + +def upload_base64_to_tos(base64_str: str): + try: + data_uri_pattern = re.compile( + r"data:image/(?Ppng|jpeg|jpg|gif|bmp|webp);base64,(?P[A-Za-z0-9+/=]+)" + ) + match = data_uri_pattern.search(base64_str) + if match: + extension = f".{match.group('ext')}" + data = match.group("data") + else: + try: + base64_str.encode("ascii") + except UnicodeEncodeError: + return None, None + data = base64_str.strip().replace("\n", "").replace("\r", "").replace(" ", "") + extension = ".png" + + data_bytes = base64.b64decode(data, validate=True) + return upload_bytes_to_tos(data_bytes, extension) + except Exception as e: + return None, f"Failed to decode base64: {str(e)}" + + +def optional_symbol(module_path: str, symbol: str): + try: + module = importlib.import_module(module_path) + return getattr(module, symbol) + except Exception: + return None + + +def init_veadk_builtin_tools(): + enabled = os.getenv("VEADK_BUILTIN_TOOLS_ENABLE", "").strip() + enabled_set = {t.strip() for t in enabled.split(",") if t.strip()} if enabled else set() + + tool_defs = [ + ("web_search", "veadk.tools.builtin_tools.web_search", "web_search"), + ("execute_skills", "veadk.tools.builtin_tools.execute_skills", "execute_skills"), + ("mcp_router", "veadk.tools.builtin_tools.mcp_router", "mcp_router"), + ("image_generate", "veadk.tools.builtin_tools.image_generate", "image_generate"), + ] + + tools = [] + for tool_id, module_path, symbol in tool_defs: + if enabled_set and tool_id not in enabled_set: + continue + tool = optional_symbol(module_path, symbol) + if tool: + tools.append(tool) + + return tools + + +def init_short_term_memory(app_name: str): + from veadk.memory import ShortTermMemory + + if os.getenv("DATABASE_POSTGRESQL_HOST"): + return ShortTermMemory(backend="postgresql") + return ShortTermMemory(backend="local") + + +def init_long_term_memory(app_name: str): + from veadk.memory import LongTermMemory + + mem_collection = os.getenv("DATABASE_VIKING_MEM_COLLECTION_NAME") + if not mem_collection: + return None + return LongTermMemory( + backend="viking_mem", + index=mem_collection, + app_name=app_name, + ) + + +def init_knowledge_base(app_name: str): + from veadk.knowledgebase import KnowledgeBase + + kb_collection = os.getenv("DATABASE_VIKING_COLLECTION_NAME") + if not kb_collection: + return None + return KnowledgeBase(backend="viking", index=kb_collection, app_name=app_name) + + +def init_prompt_manager(default_label: str = "beta"): + try: + from veadk.prompts.prompt_manager import CozeloopPromptManager + except Exception: + return None + + prompt_key = os.getenv("PROMPT_MANAGEMENT_COZELOOP_PROMPT_KEY") + token = os.getenv("PROMPT_MANAGEMENT_COZELOOP_TOKEN") + if not prompt_key or not token: + return None + return CozeloopPromptManager( + cozeloop_workspace_id=os.getenv("PROMPT_MANAGEMENT_COZELOOP_WORKSPACE_ID", ""), + cozeloop_token=token, + prompt_key=prompt_key, + label=os.getenv("PROMPT_MANAGEMENT_COZELOOP_LABEL", default_label), + ) + + +def init_observability(): + exporters = [] + try: + from veadk.tracing.telemetry.opentelemetry_tracer import OpentelemetryTracer + from veadk.tracing.telemetry.exporters.apmplus_exporter import APMPlusExporter + from veadk.tracing.telemetry.exporters.cozeloop_exporter import CozeloopExporter + from veadk.tracing.telemetry.exporters.tls_exporter import TLSExporter + except Exception: + return [] + + if os.getenv("OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT") and APMPlusExporter: + exporters.append(APMPlusExporter()) + if os.getenv("OBSERVABILITY_OPENTELEMETRY_COZELOOP_ENDPOINT") and CozeloopExporter: + exporters.append(CozeloopExporter()) + if os.getenv("OBSERVABILITY_OPENTELEMETRY_TLS_ENDPOINT") and TLSExporter: + exporters.append(TLSExporter()) + return [OpentelemetryTracer(exporters=exporters)] if exporters else [] + + +def load_local_instruction(instruction_path: Path) -> Optional[str]: + try: + if instruction_path.exists(): + return instruction_path.read_text(encoding="utf-8") + except Exception: + return None + return None + + +async def callback_cleanup_model_output(callback_context: Any, llm_response: Any): + if llm_response and getattr(llm_response, "content", None) and getattr(llm_response.content, "parts", None): + clean_parts = [] + for part in llm_response.content.parts: + is_thought = False + try: + if hasattr(part, "thought") and part.thought: + is_thought = True + elif hasattr(part, "to_dict") and part.to_dict().get("thought"): + is_thought = True + except Exception: + pass + if is_thought: + continue + + if getattr(part, "text", None): + cleaned_text = re.sub(r".*?", "", part.text, flags=re.DOTALL) + answer_match = re.search(r"(.*?)", cleaned_text, flags=re.DOTALL) + part.text = (answer_match.group(1) if answer_match else cleaned_text).strip() + + has_text = bool(getattr(part, "text", None) and part.text.strip()) + has_function = hasattr(part, "function_call") and part.function_call + if has_text or has_function: + clean_parts.append(part) + llm_response.content.parts = clean_parts + return llm_response + + +async def callback_cleanup_tool_output( + tool: Any, args: dict[str, Any], tool_context: Any, tool_response: dict +) -> Optional[dict]: + if not tool_response: + return tool_response + + def clean_data(data): + if isinstance(data, dict): + return {k: clean_data(v) for k, v in data.items()} + if isinstance(data, list): + return [clean_data(v) for v in data] + if isinstance(data, str): + pattern = r"(data:image/(?:png|jpeg|jpg|gif|bmp|webp);base64,([A-Za-z0-9+/=\s]+))" + matches = list(re.finditer(pattern, data)) + new_data = data + if matches: + for match in reversed(matches): + full_match = match.group(1) + base64_content = match.group(2) + if len(base64_content) > 1000: + url, error = upload_base64_to_tos(full_match) + replacement = f"[IMAGE_UPLOADED_TO_TOS: {url}]" if url else f"[UPLOAD_FAILED: {error}]" + start, end = match.span() + new_data = new_data[:start] + replacement + new_data[end:] + if len(new_data) > 20000: + head = new_data[:5000] + tail = new_data[-5000:] + new_data = f"{head}\n\n[... {len(new_data) - 10000} characters of logs truncated ...]\n\n{tail}" + return new_data + return data + + return clean_data(tool_response)