diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6bf9c4..e61deca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,8 +25,22 @@ jobs: - name: Install dependencies run: uv sync --extra dev - - name: Run focused tests - run: uv run --extra dev pytest tests/test_open_source_audit.py tests/test_runtime_common_packaging.py -q + - name: Run public release gate tests + run: | + uv run --extra dev pytest \ + tests/test_open_source_audit.py \ + tests/test_runtime_common_packaging.py \ + tests/test_public_positioning_docs.py \ + tests/test_prepare_ksadk_python_export.py \ + tests/test_prepare_ksadk_web_export.py \ + tests/test_tracing_setup_otlp.py \ + tests/test_check_publication_state.py \ + tests/test_check_approval_record.py \ + tests/test_public_release_gates.py \ + tests/test_markdown_repair.py \ + tests/test_conversation_runtime.py \ + tests/test_server_session_app.py \ + -q - name: Audit public repository candidate run: uv run --extra dev python scripts/open_source_audit.py --target public-repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd89be..1508e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,40 @@ # 更新日志 -本文件记录 **Kingsoft AgentEngine SDK (ksadk)** 的重要变更。 +本文件记录 **KsADK Agent Runtime Platform** 的重要变更。 格式参考 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 版本遵循 [Semantic Versioning](https://semver.org/spec/v2.0.0.html)。 +## [0.6.4] - 2026-06-10 + +### 重点 + +- **项目定位重构**:将 README、文档首页和包元数据统一为 Agent Runtime Platform 口径,突出统一运行、浏览器调试、OpenAI-Compatible API、Sandbox、部署和可观测价值。 +- **中文优先首页**:默认 README 与中文文档首页使用中文主叙述,英文 README 作为补充入口保留,避免公开首页变成英文优先材料。 +- **README 简洁化**:README 聚焦项目定位、30 秒上手、真实截图/GIF、核心能力和贡献入口;版本变更迁回 CHANGELOG 与 GitHub Releases,避免首页承担发布公告职责。 +- **真实视觉资产**:首页首屏使用真实浅色 CLI 截图,30 秒体验后展示真实本地 Web UI 截图和 GIF;演示由本地 deterministic LangGraph Runner 生成,不依赖外部模型或云环境。 +- **文档信息架构调整**:MkDocs 导航改为 Getting Started / Build / Run / Deploy / Observe / Extend / Reference,并新增 Why KsADK、Architecture、Comparison 三个认知入口页。 +- **Samples 场景入口对齐**:`ksadk-samples` 根 README 改为场景优先,真实映射 Knowledge Assistant、Workflow Agent、Tool-Using Agent 和 Memory-aware Agent;尚未实现的场景只进入 Roadmap。 +- **默认线上地域说明**:公开 README 和文档首页使用 `KSYUN_REGION=cn-beijing-6` 作为线上默认 region 示例,避免用户把非公开或内网配置照搬到公开 demo。 + +### 修复 + +- 新增 `ksadk.markdown.repair_markdown(text, enabled=True)` 可选业务侧 Markdown 形态修复工具,覆盖未闭合 fenced code block、列表/表格/代码块周边空行和换行归一化;默认关闭,运行时不自动改写 raw LLM output。用法见文档 [Agent 最佳实践 / Markdown 输出修复](public-docs/guides/agent-best-practices.md#markdown-输出修复)。 +- 清理 README、CHANGELOG、文档首页和 runtime product 文档中的环境特定表述,避免公开页面出现内部环境名、内部 header 或私有 endpoint 示例。 +- 将公开定位、视觉资产存在性、中文优先标题、敏感词扫描和 README 场景入口要求纳入本地门禁,降低后续发布材料回退风险。 +- 将 `agentengine-sdk-python` 别名包版本占用检查纳入 `public-publish-check`,避免主包与别名包发布状态不一致。 + +### 发布治理 + +- 这是修复 0.6.3 公开页面和 PyPI 元数据口径的补丁版本;已发布到 PyPI 的 0.6.3 元数据不可覆盖,因此通过新版本修复。 +- GitHub Release 页面必须保留历史版本 `v0.6.1`、`v0.6.2` 和 `v0.6.3`;0.6.4 只能新增 release,不能清理历史条目。 +- 公开 CHANGELOG 不记录非公开环境名、内网 endpoint、真实账号、真实 Skill Space ID 或临时凭证;这些只允许出现在内部联调记录里。 + +### 运行时修复 + +- `/v1/responses`、`/v1/chat/completions` 和 `RunAgentAction` 支持透传 `account_id` / `AccountId`,并写入 `PlatformInvocationContext`,便于 Skill、Workspace、Sandbox、Memory 等运行时能力按账号边界读取当前调用上下文。 +- 新增 `get_current_invocation_context_or_default()`、`get_current_user_id()` 和 `get_current_account_id()`,工具或业务代码可在当前 turn 内安全读取用户和账号上下文;无调用上下文时返回显式默认值。 + ## [0.6.3] - 2026-06-09 ### 亮点 @@ -17,7 +47,7 @@ ### 修复 - 修复 LangGraph runner 在工具调用后没有文本流式 chunk 时不会输出最终 answer,导致本地 Web UI 存储空 assistant message 的问题。 -- 修复 Skill Service KOP client 在 `KSADK_SKILL_SERVICE_REGION=pre-online` 下没有按 AgentEngine client 规则设置 `X-Ksc-Region: cn-beijing-6` 和 `X-KSC-CUSTOM-SOURCE: pre` 的问题。 +- 修复 Skill Service KOP/AICP client 在环境化路由下没有按 AgentEngine client 规则映射 region 与必要请求头的问题。 - 修复内置工具 dispatcher 遇到未知 include/tool name 时可能抛异常的问题,现在返回结构化 `unknown_tool` 错误,便于 Agent 继续解释。 - 修复 OpenClaw / Hermes deploy update payload 默认携带 `env_vars`、`storage`、`network` 等配置组的问题,降低客户更新公共镜像时误改生产配置的风险。 @@ -122,7 +152,7 @@ - 修复 Responses `input_file.file_data` / `file_url` 在会话回放中无法还原为附件展示的问题。 - 修复 conversation runtime 落库时只保存 display 文本和附件提示,导致刷新后图片变成纯文本占位的问题。 - 修复 Hosted UI 回放事件时未识别 Responses `input_file.file_data` / `input_file.file_url` 的问题。 -- 修复 server responses session mirror 在 `account_id` 为空或 PostgreSQL duplicate session 错误文本变化时可能失败,导致预发 Hosted UI 上传图片后报“连接断开或生成出错”的问题。 +- 修复 server responses session mirror 在 `account_id` 为空或 PostgreSQL duplicate session 错误文本变化时可能失败,导致 Hosted UI 上传图片后报“连接断开或生成出错”的问题。 - 修复刷新正在流式输出的会话时,订阅增量事件被单独构建成多条空“思考过程”消息的问题;恢复路径现在先合并完整 session events,再重建消息列表。 - 修复会话列表重复项、活动 invocation 判定过早失效、运行中会话锁住其他 session 切换等 UI 状态问题。 - 修复 Workspace 文件列表自动刷新时把当前 Markdown/HTML/文本预览强制切回编辑态的问题。 @@ -502,7 +532,7 @@ - 修复 Windows 离线安装时核心依赖缺失的问题。 - 修复 Windows BOM 文件兼容性,统一按 `utf-8-sig` 读取配置。 - 修复 Web UI 构建阶段 Google Fonts 资源导致的失败问题。 -- 修复预发与生产 serverless 客户端的环境路由问题。 +- 修复多环境 serverless 客户端的路由选择问题。 - 为 `fastapi` 与 `pydantic` 增加兼容性版本上限约束。 ## [0.1.0] - 2026-01-15 diff --git a/Makefile b/Makefile index db2a601..e6cbaee 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ open-source-publication-plan: open-source-publication-state: @echo "🔎 只读检查公开发布外部状态..." - @python3 scripts/check_publication_state.py --phase placeholder + @python3 scripts/check_publication_state.py --phase "$(PUBLIC_PUBLISH_PHASE)" --version "$(VERSION)" public-docs-build: @echo "📚 构建 GitHub Pages 候选文档站..." @@ -282,7 +282,7 @@ public-audit: public-secret-audit public-test: @echo "==> public tests" - @uv run --extra dev pytest tests/test_open_source_audit.py tests/test_prepare_ksadk_python_export.py tests/test_prepare_ksadk_web_export.py tests/test_runtime_common_packaging.py tests/test_tracing_setup_otlp.py -q + @uv run --extra dev pytest tests/test_open_source_audit.py tests/test_public_positioning_docs.py tests/test_prepare_ksadk_python_export.py tests/test_prepare_ksadk_web_export.py tests/test_runtime_common_packaging.py tests/test_tracing_setup_otlp.py tests/test_check_publication_state.py tests/test_check_approval_record.py tests/test_public_release_gates.py tests/test_markdown_repair.py tests/test_conversation_runtime.py tests/test_server_session_app.py -q public-build-check: clean-dist @echo "==> build and twine check" @@ -303,14 +303,14 @@ public-publish-check: python3 -c 'import json, urllib.request; targets={"repo":"$(PUBLIC_REPO)","docs":"$(PUBLIC_DOCS_URL)","pypi":"https://pypi.org/pypi/$(PUBLIC_PYPI_PROJECT)/json","alias_pypi":"https://pypi.org/pypi/$(PUBLIC_ALIAS_PYPI_PROJECT)/json"}; [print("%s: HTTP %s%s" % (name, resp.status, ("\n version=%s" % json.load(resp)["info"].get("version")) if name.endswith("pypi") else "")) for name, url in targets.items() for resp in [urllib.request.urlopen(url, timeout=20)]]'; \ fi -public-release-tag: +public-release-tag: open-source-approval-check public-preflight public-publish-check ifndef V $(error ❌ 请指定版本号,例如: make public-release-tag V=$(VERSION)) endif @branch=$$(git branch --show-current); \ if [ "$$branch" != "$(PUBLIC_BRANCH)" ]; then \ echo "❌ public release tag 必须在公开 $(PUBLIC_BRANCH) 分支创建,当前分支是 $$branch"; \ - echo " 请先完成内部审核,并将已审核候选推送到 GitHub $(PUBLIC_BRANCH)。"; \ + echo " 请先完成维护者 review,并将已审核候选推送到 GitHub $(PUBLIC_BRANCH)。"; \ exit 1; \ fi @if ! git rev-parse --verify "github/$(PUBLIC_BRANCH)" >/dev/null 2>&1; then \ @@ -446,7 +446,7 @@ clean-dist: @echo "🧹 清理 dist/build 临时产物..." @rm -rf $(DIST_DIR)/* build/ *.egg-info/ -publish: clean-dist build-only +publish: open-source-approval-check public-preflight public-publish-check clean-dist build-only @echo "🚀 发布 v$(VERSION) 到 PyPI..." @if [ -f ".pypirc" ]; then \ echo "❌ 错误: 项目根目录不允许存在 .pypirc,避免 PyPI token 进入仓库"; \ @@ -476,7 +476,7 @@ publish: clean-dist build-only python -m twine upload --config-file $(PYPIRC) $$FILES; \ fi -publish-test: clean-dist build-only +publish-test: open-source-approval-check public-preflight public-publish-check clean-dist build-only @echo "🧪 发布 v$(VERSION) 到 TestPyPI..." @if [ -f ".pypirc" ]; then \ echo "❌ 错误: 项目根目录不允许存在 .pypirc,避免 TestPyPI token 进入仓库"; \ diff --git a/README.en.md b/README.en.md index b7b5b64..98b5b09 100644 --- a/README.en.md +++ b/README.en.md @@ -1,108 +1,83 @@ -# ksadk +

KsADK

-[简体中文](README.md) | [English](README.en.md) +

Build agents once. Run them anywhere.

-[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/kingsoftcloud/ksadk-python) +

+ KsADK is the Agent Runtime Platform for AI agents. + Keep building with Google ADK, LangGraph, LangChain, or DeepAgents, then run, debug, expose, sandbox, deploy, and observe them through one runtime experience. +

-Kingsoft Cloud Agent Development Kit. `ksadk` provides the Python SDK and command line tools for building, running, packaging, and deploying AgentEngine agents. After installation, both `agentengine` and the equivalent `ksadk` command are available. KsADK covers local development, serverless runtime, ADK, LangChain/LangGraph, DeepAgents, Hermes, OpenClaw, MCP, and Skill Runtime scenarios. +

简体中文 · English

-Current version: `0.6.3`. +

+ Docs + PyPI + Ask Zread + License +

-## Install +

Real KsADK CLI screenshot: agentengine -h

-```bash -pip install -U ksadk -``` - -Install optional runtime extras when needed: +## 30 Seconds Quick Start ```bash -pip install -U "ksadk[adk]" -pip install -U "ksadk[langgraph]" -pip install -U "ksadk[deepagents]" -pip install -U "ksadk[skills]" +python -m venv .venv +source .venv/bin/activate pip install -U "ksadk[all]" -``` - -## Quick Start -The examples below use `agentengine`; you can replace it with `ksadk` for the same CLI. - -Create and run a local agent: - -```bash -agentengine init my-agent -f langgraph -cd my-agent -agentengine config +agentengine init demo-agent -f langgraph +cd demo-agent +agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini agentengine run -i ``` -Deploy to AgentEngine serverless runtime: +Start the local debugging Web UI: ```bash -agentengine launch . --target serverless +agentengine web . --no-open ``` -Open the hosted dashboard: +

Real KsADK Web UI debugging screenshot

-```bash -agentengine dashboard open -``` - -## What Is Included - -- Local development commands: `init`, `config`, `run`, `web` -- Build and deploy commands: `build`, `deploy`, `launch` -- Remote operations: `agent invoke`, `files`, `dashboard` -- Runtime integrations: ADK, LangChain, LangGraph, DeepAgents, MCP -- Hosted runtime assets: Hermes and OpenClaw -- Skill Runtime: Skill Space discovery, zip download, `sha256` verification, safe extraction, instruction loading, and workflow execution through `local_process` or E2B sandbox backends -- Built-in AgentEngine tools: skill discovery/loading, workspace file operations, component status, sandbox status, and sandbox direct code/command execution -- Sandbox Runtime: common sandbox abstraction with an E2B-compatible backend - -## 0.6.3 Highlights +

Real local Web UI demo

-- Hosted UI links align with the latest gateway / server `/hosted-ui/chat/`, share link, SSE subscription, and native terminal proxy contracts; `agentengine dashboard open` keeps using hosted entrypoints while `agentengine web` stays focused on local debugging. -- LangGraph runner now emits the final answer after tool calls even when the model does not stream text chunks, avoiding empty assistant messages in the local Web UI. -- Skill Service supports `KSADK_SKILL_SERVICE_REGION=pre-online` KOP routing with the expected pre-online headers. -- OpenClaw and Hermes updates preserve existing server-side env, storage, network, and memory configuration by default, and only overwrite those groups when matching CLI options are supplied explicitly. -- `ksadk.toolsets`, Tool Gateway, Skill Runtime, and Skill Service files are included in the package so the recommended LangGraph demo works after a clean install. +## Why KsADK -## 0.6.2 Highlights +Most agent frameworks solve how to build agents. KsADK solves how to run, debug, deploy, and observe them. -- Skill Runtime can discover Skill Space entries, download and verify skill packages, load `SKILL.md`, and execute workflow-style skills through `local_process` or E2B sandbox backends. -- `ksadk.toolsets` provides Skill, Workspace, Platform, and Sandbox built-in tools; the recommended binding pattern is `get_agentengine_tools(include=["focused", "agentengine_tool_dispatcher"])`, with lower-frequency or higher-risk tools called through dispatcher `list` / `describe` / `call`. -- Tool Gateway provides a shared `approval_required` envelope for medium/high-risk operations such as workspace writes/deletes, Skill Runtime execution, and sandbox command/code execution. -- Workspace tools now include exact snippet editing and lightweight lint checks; Sandbox tools add direct `run_command` / `run_code` and only execute through the configured isolated sandbox backend. -- `setup_tracing()` now prefers standard `OTEL_EXPORTER_OTLP_*` HTTP traces configuration while existing Langfuse environment variables remain compatible. -- The environment registry and public docs cover OTLP traces, AICP endpoint mode, Skill Service endpoint/scheme overrides, Sandbox Runtime, Skill Runtime, and Tool Gateway settings. +- Local development: `agentengine init`, `agentengine run`, `agentengine web`. +- Unified debugging: browser Web UI, streaming, attachments, workspace files, tool calls, and sessions. +- Unified protocol: local `/v1/responses` and `/v1/chat/completions`. +- Tool boundaries: Skill Runtime, Workspace, Sandbox, Memory, Knowledge. +- Engineering workflow: packaging, deployment, OpenTelemetry observability. -## Documentation +## Architecture -The public documentation site is hosted on GitHub Pages: +

KsADK Agent Runtime Platform architecture

-- [Documentation](https://kingsoftcloud.github.io/ksadk-python/en/) -- [中文文档](https://kingsoftcloud.github.io/ksadk-python/zh/) -- [Quickstart](https://kingsoftcloud.github.io/ksadk-python/en/getting-started/quickstart/) -- [Configuration](https://kingsoftcloud.github.io/ksadk-python/en/getting-started/configuration/) -- [CLI Reference](https://kingsoftcloud.github.io/ksadk-python/en/reference/cli/) -- [OpenAI-compatible API](https://kingsoftcloud.github.io/ksadk-python/en/reference/openai-compatible-api/) -- [Contributing](https://github.com/kingsoftcloud/ksadk-python/blob/main/CONTRIBUTING.md) -- [Security Policy](https://github.com/kingsoftcloud/ksadk-python/blob/main/SECURITY.md) +## Docs And Examples -The site is built with MkDocs Material, matching the documentation stack used -by Google ADK and Volcengine VEADK. +- Documentation: +- Quick Start: +- Why KsADK: +- Architecture: +- Ecosystem Positioning: +- Observability: +- Samples: -## Project Links +## Related Projects -- Documentation: -- Repository: -- Samples repository: +- KsADK repository: - Web UI repository: +- Wiki: - PyPI: -## Notes +## Contributing + +Issues, pull requests, samples, and documentation improvements are welcome. Before submitting, run: + +```bash +make public-preflight +``` -- Skill registration, CRUD, and version governance belong to Skill Service. `ksadk` consumes Skill Center at runtime. -- Sandbox template and instance lifecycle belong to Sandbox Service. `ksadk` uses the configured sandbox backend to execute runtime workflows. -- E2B-compatible sandbox backend uses the native `E2B_API_URL` and `E2B_API_KEY` environment variables. +License: Apache-2.0. diff --git a/README.md b/README.md index 1ea8b03..914fcbe 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,83 @@ -# ksadk +

KsADK

-[简体中文](README.md) | [English](README.en.md) +

一次构建 Agent,到处运行。

-[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/kingsoftcloud/ksadk-python) +

+ KsADK 是面向 AI Agent 的运行时平台(Agent Runtime Platform)。 + 继续使用 Google ADK、LangGraph、LangChain 或 DeepAgents 编写业务 Agent,再用统一 CLI、Web UI、OpenAI-Compatible API、工具运行时、沙箱、部署和可观测链路把它跑起来。 +

-金山云 Agent Development Kit。`ksadk` 提供 Python SDK 和命令行, -安装后可使用 `agentengine` 或等价的 `ksadk` 命令创建、运行、调试、 -打包和部署 AgentEngine 智能体项目。它面向 -本地开发、Serverless 运行时、Google ADK、LangChain/LangGraph、 -DeepAgents、Hermes、OpenClaw、MCP 和 Skill Runtime 等场景。 +

简体中文(默认) · English

-当前版本:`0.6.3`。 +

+ Docs + PyPI + Ask Zread + License +

-## 安装 +

KsADK 真实 CLI 截图:agentengine -h

-```bash -pip install -U ksadk -``` - -按需安装可选运行时依赖: +## 30 秒快速体验 ```bash -pip install -U "ksadk[adk]" -pip install -U "ksadk[langgraph]" -pip install -U "ksadk[deepagents]" -pip install -U "ksadk[skills]" +python -m venv .venv +source .venv/bin/activate pip install -U "ksadk[all]" -``` - -## 快速开始 -以下示例使用 `agentengine`;所有命令也可以把 `agentengine` 替换为 `ksadk`。 - -创建并运行一个本地 Agent: - -```bash -agentengine init my-agent -f langgraph -cd my-agent -agentengine config +agentengine init demo-agent -f langgraph +cd demo-agent +agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini agentengine run -i ``` -打开本地 Web UI: +启动本地调试 Web UI: ```bash agentengine web . --no-open ``` -如需走部署形态,先使用 dry-run 或内部审核流程确认配置: - -```bash -agentengine launch . --target serverless -``` - -## 包含能力 +

KsADK 真实 Web UI 调试截图

-- 本地开发命令:`init`、`config`、`run`、`web` -- 构建与部署命令:`build`、`deploy`、`launch` -- 远程操作:`agent invoke`、`files`、`dashboard` -- 运行时集成:ADK、LangChain、LangGraph、DeepAgents、MCP -- 托管运行时资产:Hermes 和 OpenClaw -- Skill Runtime:Skill Space 发现、zip 下载、`sha256` 校验、安全解压、instruction 加载,以及 `local_process` 或 E2B sandbox workflow 执行 -- AgentEngine 内置工具:skill 发现/加载、workspace 文件操作、component status、sandbox status 和 sandbox direct code/command execution -- Sandbox Runtime:通用沙箱抽象与 E2B 兼容后端 +

KsADK 真实本地 Web UI 演示

-## 0.6.3 重点 +## 为什么需要 KsADK -- Hosted UI 与最新 gateway / server 对齐 `/hosted-ui/chat/`、share link、SSE 订阅和 native terminal 代理契约;`agentengine dashboard open` 继续优先打开托管入口,本地 `agentengine web` 保持调试用途。 -- LangGraph runner 在工具调用后即使没有文本流式 chunk,也会输出最终 answer,避免本地 Web UI 出现空 assistant message。 -- Skill Service 支持 `KSADK_SKILL_SERVICE_REGION=pre-online` 的 KOP 路由,自动设置预发所需 header。 -- OpenClaw / Hermes 更新已有实例时默认保留服务端已有 env、storage、network、memory 配置,只在显式传入对应 CLI 参数时覆盖。 -- `ksadk.toolsets`、Tool Gateway、Skill Runtime 与 Skill Service 相关文件纳入发布包,主推 LangGraph demo 可以在干净安装后直接绑定 AgentEngine 内置工具。 +大多数 Agent 框架解决“如何开发 Agent”。KsADK 解决“如何运行、调试、部署和观测 Agent”。 -## 0.6.2 重点 +- 本地开发:`agentengine init`、`agentengine run`、`agentengine web`。 +- 统一调试:浏览器 Web UI、streaming、附件、workspace 文件、工具调用和会话。 +- 统一协议:本地 `/v1/responses` 与 `/v1/chat/completions`。 +- 工具边界:Skill Runtime、Workspace、Sandbox、Memory、Knowledge。 +- 工程链路:打包、部署、OpenTelemetry 可观测。 -- Skill Runtime 支持 Skill Space 远端发现、按需下载、`sha256` 校验、安全解压、`SKILL.md` instruction 加载,以及 `local_process` / E2B sandbox backend workflow 执行。 -- `ksadk.toolsets` 提供 Skill、Workspace、Platform、Sandbox 内置工具;推荐绑定 `get_agentengine_tools(include=["focused", "agentengine_tool_dispatcher"])`,把低频或高风险工具放进 dispatcher 按需 `list` / `describe` / `call`。 -- Tool Gateway 为 workspace 写入/删除、Skill Runtime 执行、sandbox command/code 等中高风险操作提供统一 `approval_required` envelope。 -- Workspace tools 新增 exact snippet edit 与 lightweight lint;Sandbox tools 新增 direct `run_command` / `run_code`,且只通过 configured isolated sandbox backend 执行。 -- `setup_tracing()` 优先识别标准 `OTEL_EXPORTER_OTLP_*` HTTP traces 配置,Langfuse 环境变量仍保持兼容。 -- 环境变量 registry 和公开文档覆盖 OTLP traces、AICP endpoint mode、Skill Service endpoint/scheme、Sandbox Runtime、Skill Runtime 和 Tool Gateway settings。 +## 架构 -## 文档 +

KsADK Agent Runtime Platform 架构

-公开文档托管在 GitHub Pages,并使用 MkDocs Material 与双语 i18n 方案: +## 文档与样例 -- [中文文档](https://kingsoftcloud.github.io/ksadk-python/zh/) -- [English documentation](https://kingsoftcloud.github.io/ksadk-python/en/) -- [快速开始](https://kingsoftcloud.github.io/ksadk-python/getting-started/quickstart/) -- [配置项](https://kingsoftcloud.github.io/ksadk-python/getting-started/configuration/) -- [命令行参考](https://kingsoftcloud.github.io/ksadk-python/reference/cli/) -- [OpenAI 兼容 API](https://kingsoftcloud.github.io/ksadk-python/reference/openai-compatible-api/) -- [贡献指南](https://github.com/kingsoftcloud/ksadk-python/blob/main/CONTRIBUTING.md) -- [安全策略](https://github.com/kingsoftcloud/ksadk-python/blob/main/SECURITY.md) +- 文档: +- 快速开始: +- 为什么需要 KsADK: +- 架构: +- 生态定位对比: +- 可观测: +- 样例仓库: -## 项目链接 +## 相关项目 -- 文档: -- 仓库: -- wiki: -- 示例仓库: +- KsADK 仓库: - Web UI 仓库: +- Wiki: - PyPI: -- 开源协议:Apache-2.0 -## 说明 +## 参与贡献 + +欢迎通过 issue、PR、样例和文档改进参与贡献。提交前建议运行: + +```bash +make public-preflight +``` -- Skill 注册、CRUD 和版本治理属于 Skill Service;`ksadk` 在运行时消费 Skill Center。 -- Sandbox 模板和实例生命周期属于 Sandbox Service;`ksadk` 使用配置的沙箱后端执行运行时工作流。 -- E2B 兼容沙箱后端使用原生 `E2B_API_URL` 和 `E2B_API_KEY` 环境变量。 +开源协议:Apache-2.0。 diff --git a/README.zh-CN.md b/README.zh-CN.md index 1ea8b03..914fcbe 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,111 +1,83 @@ -# ksadk +

KsADK

-[简体中文](README.md) | [English](README.en.md) +

一次构建 Agent,到处运行。

-[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/kingsoftcloud/ksadk-python) +

+ KsADK 是面向 AI Agent 的运行时平台(Agent Runtime Platform)。 + 继续使用 Google ADK、LangGraph、LangChain 或 DeepAgents 编写业务 Agent,再用统一 CLI、Web UI、OpenAI-Compatible API、工具运行时、沙箱、部署和可观测链路把它跑起来。 +

-金山云 Agent Development Kit。`ksadk` 提供 Python SDK 和命令行, -安装后可使用 `agentengine` 或等价的 `ksadk` 命令创建、运行、调试、 -打包和部署 AgentEngine 智能体项目。它面向 -本地开发、Serverless 运行时、Google ADK、LangChain/LangGraph、 -DeepAgents、Hermes、OpenClaw、MCP 和 Skill Runtime 等场景。 +

简体中文(默认) · English

-当前版本:`0.6.3`。 +

+ Docs + PyPI + Ask Zread + License +

-## 安装 +

KsADK 真实 CLI 截图:agentengine -h

-```bash -pip install -U ksadk -``` - -按需安装可选运行时依赖: +## 30 秒快速体验 ```bash -pip install -U "ksadk[adk]" -pip install -U "ksadk[langgraph]" -pip install -U "ksadk[deepagents]" -pip install -U "ksadk[skills]" +python -m venv .venv +source .venv/bin/activate pip install -U "ksadk[all]" -``` - -## 快速开始 -以下示例使用 `agentengine`;所有命令也可以把 `agentengine` 替换为 `ksadk`。 - -创建并运行一个本地 Agent: - -```bash -agentengine init my-agent -f langgraph -cd my-agent -agentengine config +agentengine init demo-agent -f langgraph +cd demo-agent +agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini agentengine run -i ``` -打开本地 Web UI: +启动本地调试 Web UI: ```bash agentengine web . --no-open ``` -如需走部署形态,先使用 dry-run 或内部审核流程确认配置: - -```bash -agentengine launch . --target serverless -``` - -## 包含能力 +

KsADK 真实 Web UI 调试截图

-- 本地开发命令:`init`、`config`、`run`、`web` -- 构建与部署命令:`build`、`deploy`、`launch` -- 远程操作:`agent invoke`、`files`、`dashboard` -- 运行时集成:ADK、LangChain、LangGraph、DeepAgents、MCP -- 托管运行时资产:Hermes 和 OpenClaw -- Skill Runtime:Skill Space 发现、zip 下载、`sha256` 校验、安全解压、instruction 加载,以及 `local_process` 或 E2B sandbox workflow 执行 -- AgentEngine 内置工具:skill 发现/加载、workspace 文件操作、component status、sandbox status 和 sandbox direct code/command execution -- Sandbox Runtime:通用沙箱抽象与 E2B 兼容后端 +

KsADK 真实本地 Web UI 演示

-## 0.6.3 重点 +## 为什么需要 KsADK -- Hosted UI 与最新 gateway / server 对齐 `/hosted-ui/chat/`、share link、SSE 订阅和 native terminal 代理契约;`agentengine dashboard open` 继续优先打开托管入口,本地 `agentengine web` 保持调试用途。 -- LangGraph runner 在工具调用后即使没有文本流式 chunk,也会输出最终 answer,避免本地 Web UI 出现空 assistant message。 -- Skill Service 支持 `KSADK_SKILL_SERVICE_REGION=pre-online` 的 KOP 路由,自动设置预发所需 header。 -- OpenClaw / Hermes 更新已有实例时默认保留服务端已有 env、storage、network、memory 配置,只在显式传入对应 CLI 参数时覆盖。 -- `ksadk.toolsets`、Tool Gateway、Skill Runtime 与 Skill Service 相关文件纳入发布包,主推 LangGraph demo 可以在干净安装后直接绑定 AgentEngine 内置工具。 +大多数 Agent 框架解决“如何开发 Agent”。KsADK 解决“如何运行、调试、部署和观测 Agent”。 -## 0.6.2 重点 +- 本地开发:`agentengine init`、`agentengine run`、`agentengine web`。 +- 统一调试:浏览器 Web UI、streaming、附件、workspace 文件、工具调用和会话。 +- 统一协议:本地 `/v1/responses` 与 `/v1/chat/completions`。 +- 工具边界:Skill Runtime、Workspace、Sandbox、Memory、Knowledge。 +- 工程链路:打包、部署、OpenTelemetry 可观测。 -- Skill Runtime 支持 Skill Space 远端发现、按需下载、`sha256` 校验、安全解压、`SKILL.md` instruction 加载,以及 `local_process` / E2B sandbox backend workflow 执行。 -- `ksadk.toolsets` 提供 Skill、Workspace、Platform、Sandbox 内置工具;推荐绑定 `get_agentengine_tools(include=["focused", "agentengine_tool_dispatcher"])`,把低频或高风险工具放进 dispatcher 按需 `list` / `describe` / `call`。 -- Tool Gateway 为 workspace 写入/删除、Skill Runtime 执行、sandbox command/code 等中高风险操作提供统一 `approval_required` envelope。 -- Workspace tools 新增 exact snippet edit 与 lightweight lint;Sandbox tools 新增 direct `run_command` / `run_code`,且只通过 configured isolated sandbox backend 执行。 -- `setup_tracing()` 优先识别标准 `OTEL_EXPORTER_OTLP_*` HTTP traces 配置,Langfuse 环境变量仍保持兼容。 -- 环境变量 registry 和公开文档覆盖 OTLP traces、AICP endpoint mode、Skill Service endpoint/scheme、Sandbox Runtime、Skill Runtime 和 Tool Gateway settings。 +## 架构 -## 文档 +

KsADK Agent Runtime Platform 架构

-公开文档托管在 GitHub Pages,并使用 MkDocs Material 与双语 i18n 方案: +## 文档与样例 -- [中文文档](https://kingsoftcloud.github.io/ksadk-python/zh/) -- [English documentation](https://kingsoftcloud.github.io/ksadk-python/en/) -- [快速开始](https://kingsoftcloud.github.io/ksadk-python/getting-started/quickstart/) -- [配置项](https://kingsoftcloud.github.io/ksadk-python/getting-started/configuration/) -- [命令行参考](https://kingsoftcloud.github.io/ksadk-python/reference/cli/) -- [OpenAI 兼容 API](https://kingsoftcloud.github.io/ksadk-python/reference/openai-compatible-api/) -- [贡献指南](https://github.com/kingsoftcloud/ksadk-python/blob/main/CONTRIBUTING.md) -- [安全策略](https://github.com/kingsoftcloud/ksadk-python/blob/main/SECURITY.md) +- 文档: +- 快速开始: +- 为什么需要 KsADK: +- 架构: +- 生态定位对比: +- 可观测: +- 样例仓库: -## 项目链接 +## 相关项目 -- 文档: -- 仓库: -- wiki: -- 示例仓库: +- KsADK 仓库: - Web UI 仓库: +- Wiki: - PyPI: -- 开源协议:Apache-2.0 -## 说明 +## 参与贡献 + +欢迎通过 issue、PR、样例和文档改进参与贡献。提交前建议运行: + +```bash +make public-preflight +``` -- Skill 注册、CRUD 和版本治理属于 Skill Service;`ksadk` 在运行时消费 Skill Center。 -- Sandbox 模板和实例生命周期属于 Sandbox Service;`ksadk` 使用配置的沙箱后端执行运行时工作流。 -- E2B 兼容沙箱后端使用原生 `E2B_API_URL` 和 `E2B_API_KEY` 环境变量。 +开源协议:Apache-2.0。 diff --git a/docs/maintainer-approval-record.md b/docs/maintainer-approval-record.md new file mode 100644 index 0000000..76688ea --- /dev/null +++ b/docs/maintainer-approval-record.md @@ -0,0 +1,57 @@ +# KsADK Public Release Approval Record + +This record must be filled after maintainer review and before any external +write action, including GitHub release tags, GitHub Releases, TestPyPI, or +PyPI publication. + +## Required Approval Decisions + +| Decision | Approved value | +| --- | --- | +| License | Apache-2.0 | +| Python repository | kingsoftcloud/ksadk-python | +| Web UI repository | kingsoftcloud/ksadk-web | +| Python package version | 0.6.4 | +| Public docs URL | https://kingsoftcloud.github.io/ksadk-python/ | +| Package metadata repository URL | https://github.com/kingsoftcloud/ksadk-python | +| Package metadata documentation URL | https://kingsoftcloud.github.io/ksadk-python/ | +| Security contact | security@kingsoft.com | + +## Publication Strategy + +Record exactly one approved source publication strategy: + +| Strategy | Approved | +| --- | --- | +| Reviewed GitHub pull request | No | +| Clean export from reviewed candidate | No | +| Rewritten Git history after secret scan | No | + +The approved strategy must name the reviewed commit, tag, pull request, or +export archive used for: + +- `ksadk-python`: TBD +- `ksadk-web`: TBD + +Both approved source references must include the current commit SHA at approval +time. This prevents a stale approval record from passing after candidate +changes. + +## Required Evidence Before Approval + +- `make public-preflight` exits successfully. +- `make public-publish-check PUBLIC_PUBLISH_PHASE=pre-publish V=0.6.4` confirms + the target version is not already on PyPI. +- GitHub PR checks are green on the reviewed commit. +- Release notes and `CHANGELOG.md` were reviewed. +- Public README and docs were reviewed for sensitive environment names, + internal endpoints, tokens, customer data, and inaccurate competitor claims. +- PyPI/TestPyPI credentials stay outside the repository. + +## Approval Sign-Off + +| Role | Name | Decision | Date | +| --- | --- | --- | --- | +| Maintainer | | | | +| Security reviewer | | | | +| Release owner | | | | diff --git a/ksadk/__init__.py b/ksadk/__init__.py index c6e9543..86127bf 100644 --- a/ksadk/__init__.py +++ b/ksadk/__init__.py @@ -1,7 +1,7 @@ """ -KsADK: Kingsoft Cloud Agent Development Kit +KsADK: Agent Runtime Platform for AI agents. -支持 LangChain / LangGraph / Google ADK 多框架的本地运行与云端部署 +统一运行、调试、部署和可观测体验,支持 ADK、LangGraph、LangChain 和 DeepAgents。 """ from ksadk.version import VERSION diff --git a/ksadk/conversations/runtime.py b/ksadk/conversations/runtime.py index e0fdbe7..9b6d97d 100644 --- a/ksadk/conversations/runtime.py +++ b/ksadk/conversations/runtime.py @@ -2424,6 +2424,7 @@ async def invoke_conversation_once( request_metadata: Mapping[str, Any] | None = None, resume_input: Mapping[str, Any] | None = None, response_id: str | None = None, + account_id: str | None = None, session_service_provider: Callable[[], Any] | None = None, ) -> tuple[str, dict[str, Any]]: """非流式 turn 编排入口。 @@ -2455,6 +2456,7 @@ async def invoke_conversation_once( runtime_context = PlatformInvocationContext( agent_id=agent_id, user_id=user_id, + account_id=str(account_id or ""), session_id=prepared.session_id, history=list(prepared.history), input_content=list(prepared.input_content), @@ -2602,6 +2604,7 @@ async def _iter_conversation_turn_events( request_metadata: Mapping[str, Any] | None = None, resume_input: Mapping[str, Any] | None = None, response_id: str | None = None, + account_id: str | None = None, session_service_provider: Callable[[], Any] | None = None, ) -> AsyncIterator[dict[str, Any]]: """Internal semantic event stream shared by protocol serializers.""" @@ -2658,6 +2661,7 @@ async def _iter_conversation_turn_events( runtime_context = PlatformInvocationContext( agent_id=agent_id, user_id=user_id, + account_id=str(account_id or ""), session_id=prepared.session_id, history=list(prepared.history), input_content=list(prepared.input_content), @@ -3023,6 +3027,7 @@ async def stream_conversation_turn( instructions: Optional[str] = None, request_metadata: Mapping[str, Any] | None = None, resume_input: Mapping[str, Any] | None = None, + account_id: str | None = None, session_service_provider: Callable[[], Any] | None = None, ) -> AsyncIterator[str]: """Legacy ksadk response SSE stream used by hosted chat and chat-completions.""" @@ -3040,6 +3045,7 @@ async def stream_conversation_turn( instructions=instructions, request_metadata=request_metadata, resume_input=resume_input, + account_id=account_id, session_service_provider=session_service_provider, ): event_type = event.get("type") @@ -3110,6 +3116,7 @@ async def stream_responses_conversation_turn( instructions: Optional[str] = None, request_metadata: Mapping[str, Any] | None = None, resume_input: Mapping[str, Any] | None = None, + account_id: str | None = None, session_service_provider: Callable[[], Any] | None = None, ) -> AsyncIterator[str]: """OpenAI Responses-style SSE stream.""" @@ -3186,6 +3193,7 @@ def _next_output_index() -> int: request_metadata=request_metadata, resume_input=resume_input, response_id=response_id, + account_id=account_id, session_service_provider=session_service_provider, ): event_metadata = event.get("metadata") diff --git a/ksadk/markdown.py b/ksadk/markdown.py new file mode 100644 index 0000000..05eb631 --- /dev/null +++ b/ksadk/markdown.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from typing import Any + + +_FENCE_MARKER_CHARS = ("`", "~") +_LIST_PREFIXES = ("- ", "* ", "+ ") + + +def repair_markdown(text: Any, *, enabled: bool = False) -> str: + """尽量修复 LLM 输出中的常见 Markdown 形态问题。 + + 这是业务侧按需调用的保守修复工具。KsADK runtime 默认保留模型原文, + 不会自动调用这个函数。 + """ + + if text is None: + return "" + value = str(text) + if not enabled: + return value + if value == "": + return "" + + normalized = value.replace("\r\n", "\n").replace("\r", "\n") + lines = _strip_terminal_empty_lines(normalized.split("\n")) + if not lines: + return "" + + lines = _normalize_block_spacing(lines) + lines = _close_unclosed_fence(lines) + + return "\n".join(lines) + "\n" + + +def _strip_terminal_empty_lines(lines: list[str]) -> list[str]: + start = 0 + end = len(lines) + while start < end and lines[start] == "": + start += 1 + while end > start and lines[end - 1] == "": + end -= 1 + return lines[start:end] + + +def _normalize_block_spacing(lines: list[str]) -> list[str]: + result: list[str] = [] + in_fence = False + fence_marker = "" + previous_block = "" + + for index, line in enumerate(lines): + stripped = line.lstrip() + block = _line_block_type(line, in_fence) + + if block == "fence": + marker = _fence_marker(stripped) or "" + if not in_fence: + _append_blank_before_block(result, previous_block) + in_fence = True + fence_marker = marker + elif _is_closing_fence(marker, fence_marker): + in_fence = False + fence_marker = "" + elif not in_fence: + if block in {"table", "list"}: + _append_blank_before_block(result, previous_block) + elif block == "paragraph": + _append_blank_after_block(result, previous_block) + + result.append(line) + + if line == "": + previous_block = "" + elif not in_fence or block == "fence": + previous_block = block + + next_line = lines[index + 1] if index + 1 < len(lines) else None + if next_line is None or in_fence: + continue + next_block = _line_block_type(next_line, in_fence) + if block in {"table", "list"} and next_block == "paragraph": + _append_blank_once(result) + previous_block = "" + + return result + + +def _append_blank_before_block(result: list[str], previous_block: str) -> None: + if not result or result[-1] == "": + return + if previous_block in {"", "table", "list", "fence"}: + return + result.append("") + + +def _append_blank_after_block(result: list[str], previous_block: str) -> None: + if result and result[-1] != "" and previous_block in {"list", "table", "fence"}: + result.append("") + + +def _append_blank_once(result: list[str]) -> None: + if result and result[-1] != "": + result.append("") + + +def _close_unclosed_fence(lines: list[str]) -> list[str]: + open_marker = "" + for line in lines: + marker = _fence_marker(line.lstrip()) + if marker is None: + continue + if not open_marker: + open_marker = marker + elif _is_closing_fence(marker, open_marker): + open_marker = "" + if open_marker: + return [*lines, open_marker] + return lines + + +def _line_block_type(line: str, in_fence: bool) -> str: + if line == "": + return "" + stripped = line.lstrip() + if _fence_marker(stripped): + return "fence" + if in_fence: + return "code" + if _is_table_line(stripped): + return "table" + if _is_list_line(stripped): + return "list" + return "paragraph" + + +def _fence_marker(stripped_line: str) -> str | None: + for char in _FENCE_MARKER_CHARS: + if not stripped_line.startswith(char * 3): + continue + length = 0 + for value in stripped_line: + if value != char: + break + length += 1 + return char * length + return None + + +def _is_closing_fence(candidate: str, open_marker: str) -> bool: + return ( + bool(candidate) + and bool(open_marker) + and candidate[0] == open_marker[0] + and len(candidate) >= len(open_marker) + ) + + +def _is_table_line(stripped_line: str) -> bool: + return stripped_line.startswith("|") and stripped_line.endswith("|") and stripped_line.count("|") >= 2 + + +def _is_list_line(stripped_line: str) -> bool: + if stripped_line.startswith(_LIST_PREFIXES): + return True + dot_index = stripped_line.find(". ") + if dot_index <= 0: + return False + return stripped_line[:dot_index].isdigit() diff --git a/ksadk/runtime_context.py b/ksadk/runtime_context.py index e6c304e..ea52d7a 100644 --- a/ksadk/runtime_context.py +++ b/ksadk/runtime_context.py @@ -21,6 +21,7 @@ class PlatformInvocationContext: current_attachment_results: list[dict[str, Any]] has_current_files: bool runner_type: str + account_id: str = "" model: str | None = None model_options: dict[str, Any] | None = None kb_context: dict[str, Any] | None = None @@ -30,6 +31,7 @@ def to_payload(self) -> dict[str, Any]: return { "agent_id": self.agent_id, "user_id": self.user_id, + "account_id": self.account_id, "session_id": self.session_id, "history": list(self.history or []), "input_content": list(self.input_content or []), @@ -56,6 +58,42 @@ def get_current_invocation_context() -> PlatformInvocationContext | None: return _CURRENT_PLATFORM_INVOCATION_CONTEXT.get() +def get_current_invocation_context_or_default() -> PlatformInvocationContext: + context = get_current_invocation_context() + if context is not None: + return context + return PlatformInvocationContext( + agent_id="", + user_id="", + account_id="", + session_id="", + history=[], + input_content=[], + input_messages=[], + input_parts=[], + attachments=[], + attachment_results=[], + current_attachments=[], + current_attachment_results=[], + has_current_files=False, + runner_type="", + ) + + +def get_current_user_id(default: str = "") -> str: + context = get_current_invocation_context() + if context is None: + return default + return str(context.user_id or default) + + +def get_current_account_id(default: str = "") -> str: + context = get_current_invocation_context() + if context is None: + return default + return str(context.account_id or default) + + def set_current_invocation_context( context: PlatformInvocationContext | None, ) -> Token[PlatformInvocationContext | None]: diff --git a/ksadk/server/app.py b/ksadk/server/app.py index 055ac69..56077b2 100644 --- a/ksadk/server/app.py +++ b/ksadk/server/app.py @@ -634,6 +634,7 @@ class RunAgentActionRequest(BaseModel): AgentId: str Messages: List[Dict[str, Any]] = Field(default_factory=list) UserId: Optional[str] = "user" + AccountId: Optional[str] = None SessionId: Optional[str] = None ApiFormat: str = "responses" Stream: bool = False @@ -669,6 +670,7 @@ class ResponsesRequest(BaseModel): safety_identifier: Optional[str] = None prompt_cache_key: Optional[str] = None user: Optional[str] = None + account_id: Optional[str] = None store: Optional[bool] = None previous_response_id: Optional[str] = None stream: bool = False @@ -1374,6 +1376,7 @@ async def list_openai_models(): async def run_agent_action(request: RunAgentActionRequest): api_format = (request.ApiFormat or "responses").strip().lower() run_user_id = _clean_optional_string(request.UserId) or "user" + account_id = _clean_optional_string(request.AccountId) resume_input = ( conversation.extract_responses_resume_input(request.ResponsesInput) if request.ResponsesInput is not None @@ -1397,6 +1400,7 @@ async def run_agent_action(request: RunAgentActionRequest): stream=True, session_id=request.SessionId, user=run_user_id, + account_id=account_id, ) return await chat_completions(completion_request) return _detached_streaming_response( @@ -1411,6 +1415,7 @@ async def run_agent_action(request: RunAgentActionRequest): model_options=request.ModelOptions, request_metadata=request_metadata, resume_input=resume_input, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ) @@ -1431,6 +1436,7 @@ async def run_agent_action(request: RunAgentActionRequest): request_metadata=request_metadata, resume_input=resume_input, response_id=responses_response_id, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ) @@ -2079,6 +2085,7 @@ class ChatCompletionRequest(BaseModel): stream: bool = False session_id: Optional[str] = None user: Optional[str] = None + account_id: Optional[str] = None temperature: Optional[float] = 0.7 max_tokens: Optional[int] = None @@ -2105,6 +2112,7 @@ async def responses(request: ResponsesRequest): request_metadata.setdefault("conversation", request.conversation) if request.store is not None: request_metadata.setdefault("store", request.store) + account_id = _clean_optional_string(request.account_id) if request.stream: return StreamingResponse( @@ -2120,6 +2128,7 @@ async def responses(request: ResponsesRequest): instructions=request.instructions, request_metadata=request_metadata, resume_input=resume_input, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ), @@ -2140,6 +2149,7 @@ async def responses(request: ResponsesRequest): request_metadata=request_metadata, resume_input=resume_input, response_id=response_id, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ) @@ -2159,6 +2169,7 @@ async def chat_completions(request: ChatCompletionRequest): messages = conversation.normalize_kop_messages(request.messages) agent_id = active_runner.detection_result.name resolved_user_id = _clean_optional_string(request.user) or "user" + account_id = _clean_optional_string(request.account_id) if request.stream: return StreamingResponse( @@ -2171,6 +2182,7 @@ async def chat_completions(request: ChatCompletionRequest): model=request.model, model_metadata=request.model_metadata, model_options=request.model_options, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ), @@ -2186,6 +2198,7 @@ async def chat_completions(request: ChatCompletionRequest): model=request.model, model_metadata=request.model_metadata, model_options=request.model_options, + account_id=account_id, prepare_runner=_prepare_runner_for_model, session_service_provider=resolve_session_service, ) diff --git a/ksadk/version.py b/ksadk/version.py index 8ca3610..1ebdd1a 100644 --- a/ksadk/version.py +++ b/ksadk/version.py @@ -1,4 +1,4 @@ """KsADK 版本信息""" -VERSION = "0.6.3" +VERSION = "0.6.4" __version__ = VERSION diff --git a/mkdocs.yml b/mkdocs.yml index 69dba88..adc8e69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: KsADK -site_description: 金山云智能体开发套件 +site_description: AI Agent Runtime Platform site_url: https://kingsoftcloud.github.io/ksadk-python/ repo_url: https://github.com/kingsoftcloud/ksadk-python repo_name: kingsoftcloud/ksadk-python @@ -25,43 +25,48 @@ theme: - search.share nav: - - 欢迎: index.md - - 快速开始: + - Getting Started 入门: + - 首页: index.md + - 为什么需要 KsADK: getting-started/why-ksadk.md + - 架构: getting-started/architecture.md + - 生态定位对比: getting-started/comparison.md - 初识 KsADK: getting-started/concepts.md - 快速开始: getting-started/quickstart.md - 配置项: getting-started/configuration.md - 项目结构: getting-started/project-structure.md - - 教程: + - Build 构建: - 构建 LangGraph 智能体: tutorials/langgraph-agent.md - 构建 ADK 智能体: tutorials/adk-agent.md - 接入已有智能体: tutorials/existing-agent.md - - 指南: + - 框架接入: guides/frameworks.md + - Agent 最佳实践: guides/agent-best-practices.md + - Run 运行: - 本地 Web UI: guides/local-web-ui.md - Web UI 仓库: guides/web-ui-source.md - 运行时架构: guides/runtime-architecture.md - - 运行时产品: guides/runtime-products.md - 智能体上下文: guides/agent-context.md - - Agent 最佳实践: guides/agent-best-practices.md - - 框架接入: guides/frameworks.md - - 工具与 Skill Runtime: guides/tools-and-skill-runtime.md - - 记忆与知识库: guides/memory-knowledge.md - 附件与多模态输入: guides/attachments-multimodal.md - 工作区文件: guides/workspace-files.md - - 可观测与链路追踪: guides/observability-tracing.md + - 会话与文件: reference/runtime-sessions-files.md + - Deploy 部署: - 构建与打包: guides/build-and-package.md - - 参考: - - 命令行: reference/cli.md + - 运行时产品: guides/runtime-products.md + - 远程运行时 API: reference/remote-runtime-api.md + - 发布流程: contributing/release.md + - Observe 观测: + - 可观测与链路追踪: guides/observability-tracing.md + - 故障排查: reference/troubleshooting.md + - 测试策略: contributing/testing.md + - Extend 扩展: + - 工具与 Skill Runtime: guides/tools-and-skill-runtime.md + - 记忆与知识库: guides/memory-knowledge.md - OpenAI 兼容 API: reference/openai-compatible-api.md + - 贡献指南: contributing/index.md + - Reference 参考: + - 命令行: reference/cli.md - 项目配置: reference/project-config.md - - 远程运行时 API: reference/remote-runtime-api.md - 环境变量: reference/environment-variables.md - - 会话与文件: reference/runtime-sessions-files.md - 安全边界: reference/security-boundaries.md - - 故障排查: reference/troubleshooting.md - - 贡献: - - 贡献指南: contributing/index.md - - 测试策略: contributing/testing.md - - 发布流程: contributing/release.md exclude_docs: | internal/ @@ -81,53 +86,59 @@ plugins: build: true default: true site_name: KsADK - site_description: 金山云智能体开发套件 + site_description: AI Agent Runtime Platform theme: language: zh - locale: en name: English build: true site_name: KsADK - site_description: Kingsoft Cloud Agent Development Kit + site_description: Agent Runtime Platform for AI agents theme: language: en nav_translations: - 欢迎: Overview - 快速开始: Getting Started + Getting Started 入门: Getting Started + 首页: Overview + 为什么需要 KsADK: Why KsADK + 架构: Architecture + 生态定位对比: Ecosystem Positioning 初识 KsADK: Concepts + 快速开始: Quick Start 配置项: Configuration 项目结构: Project Structure - 教程: Tutorials + Build 构建: Build 构建 LangGraph 智能体: Build A LangGraph Agent 构建 ADK 智能体: Build An ADK Agent 接入已有智能体: Bring An Existing Agent - 指南: Guides + Run 运行: Run 本地 Web UI: Local Web UI Web UI 仓库: Web UI Repository 运行时架构: Runtime Architecture - 运行时产品: Runtime Products 智能体上下文: Agent Context - Agent 最佳实践: Agent Best Practices - 框架接入: Frameworks - 工具与 Skill Runtime: Tools And Skill Runtime - 记忆与知识库: Memory And Knowledge 附件与多模态输入: Attachments And Multimodal Input 工作区文件: Workspace Files - 可观测与链路追踪: Observability And Tracing + 会话与文件: Runtime Sessions And Files + Deploy 部署: Deploy 构建与打包: Build And Package - 参考: Reference + 运行时产品: Runtime Products + 远程运行时 API: Remote Runtime API + 发布流程: Release Process + Observe 观测: Observe + 可观测与链路追踪: Observability And Tracing + 故障排查: Troubleshooting + 测试策略: Testing Strategy + Extend 扩展: Extend + 工具与 Skill Runtime: Tools And Skill Runtime + 记忆与知识库: Memory And Knowledge 命令行: CLI OpenAI 兼容 API: OpenAI-Compatible API + 贡献指南: Contributing Guide + Reference 参考: Reference 项目配置: Project Configuration - 远程运行时 API: Remote Runtime API 环境变量: Environment Variables - 会话与文件: Runtime Sessions And Files 安全边界: Security Boundaries - 故障排查: Troubleshooting - 贡献: Contributing - 贡献指南: Contributing Guide - 测试策略: Testing Strategy - 发布流程: Release Process + Agent 最佳实践: Agent Best Practices + 框架接入: Frameworks - mkdocstrings: handlers: python: diff --git a/public-docs/assets/ksadk-local-debugging-demo.gif b/public-docs/assets/ksadk-local-debugging-demo.gif new file mode 100644 index 0000000..6e33659 Binary files /dev/null and b/public-docs/assets/ksadk-local-debugging-demo.gif differ diff --git a/public-docs/assets/ksadk-runtime-architecture.png b/public-docs/assets/ksadk-runtime-architecture.png new file mode 100644 index 0000000..8182a30 Binary files /dev/null and b/public-docs/assets/ksadk-runtime-architecture.png differ diff --git a/public-docs/assets/ksadk-runtime-architecture.svg b/public-docs/assets/ksadk-runtime-architecture.svg new file mode 100644 index 0000000..ad955c4 --- /dev/null +++ b/public-docs/assets/ksadk-runtime-architecture.svg @@ -0,0 +1,109 @@ + + KsADK Agent Runtime Platform 架构图 + 从 Agent 代码到 KsADK SDK、统一运行时、Skill Runtime、Workspace、Sandbox、Memory Knowledge、AgentEngine、Serverless、Hermes 和 OpenClaw 的运行链路。 + + + + + + + + + + + + + + + + + + + + + + + KsADK Agent Runtime Platform + 一次构建 Agent,到处运行:统一开发、调试、运行、沙箱、部署和观测体验 + + + AGENT CODE + + + Google ADK + + LangGraph + + LangChain + + DeepAgents + + + + + + + KsADK SDK + Runner 适配 / 配置管理 / Toolsets / 项目打包 + + + + + + + 统一运行时 + CLI / Browser Web UI / OpenAI-Compatible API / Streaming Sessions + 本地开发时即验证部署后的运行边界 + + + + + + + + + + + Skill Runtime + Skill Space / workflow + + + Workspace + 会话文件 / artifacts + + + Sandbox + 隔离命令 / 代码执行 + + + Memory & Knowledge + 长期记忆 / 知识库 + + + + + + + + + + AgentEngine + 远端运行、服务入口与平台能力 + + + + + + + Serverless / Hermes / OpenClaw Runtime + + diff --git a/public-docs/assets/ksadk-runtime-platform-hero-wide.png b/public-docs/assets/ksadk-runtime-platform-hero-wide.png new file mode 100644 index 0000000..4bab373 Binary files /dev/null and b/public-docs/assets/ksadk-runtime-platform-hero-wide.png differ diff --git a/public-docs/assets/ksadk-runtime-platform-hero.png b/public-docs/assets/ksadk-runtime-platform-hero.png new file mode 100644 index 0000000..20af009 Binary files /dev/null and b/public-docs/assets/ksadk-runtime-platform-hero.png differ diff --git a/public-docs/assets/ksadk-web-ui-screenshot.png b/public-docs/assets/ksadk-web-ui-screenshot.png new file mode 100644 index 0000000..e3ca8de Binary files /dev/null and b/public-docs/assets/ksadk-web-ui-screenshot.png differ diff --git a/public-docs/contributing/release.en.md b/public-docs/contributing/release.en.md index 9e0f636..5aea59b 100644 --- a/public-docs/contributing/release.en.md +++ b/public-docs/contributing/release.en.md @@ -6,24 +6,20 @@ The first public release should be prepared from an independent branch and revie 1. Prepare the candidate on an independent branch. 2. Run local tests, packaging checks, artifact audits, and docs build. -3. Push to the internal ezone repository for company review. -4. After approval, import the reviewed source into GitHub `main`. +3. Push the candidate pull request for maintainer review. +4. After approval, merge the reviewed source into GitHub `main`. 5. Enable GitHub Pages only after public docs CI passes. 6. Create release tags, GitHub release assets, and TestPyPI/PyPI uploads only from the reviewed GitHub `main` commit after public CI passes. -7. Verify published releases and tags with - `python3 scripts/check_publication_state.py --phase published --check-release --check-pages`. - -GitHub source import must not happen before the internal review gate. The -candidate branch is for review and local gates only; public release assets must -not be created directly from an internal `master` branch or an unsynced -`release/public-*` candidate branch. -The published-state check also verifies that the imported GitHub repositories -contain the expected public source, CI workflow, and Pages workflow marker -files after the placeholder file is removed. -Release-state checks also verify that the Python GitHub release includes the -reviewed sdist and wheel assets and that release notes record the pinned -`ksadk-web` release. +7. Before publication, verify the external state with + `make public-publish-check PUBLIC_PUBLISH_PHASE=pre-publish V=0.6.4`. +8. After publication, verify the external state with + `make public-publish-check PUBLIC_PUBLISH_PHASE=post-publish V=0.6.4`. + +Public release assets must not be created directly from an unsynced candidate +branch. `make publish`, `make publish-test`, and `make public-release-tag` +require `docs/maintainer-approval-record.md` to name the reviewed commit, +publication strategy, and maintainer sign-offs. ## Required Evidence @@ -59,13 +55,10 @@ before importing source into GitHub. The release tag, GitHub release, package version, and documentation version should refer to the same reviewed GitHub `main` commit. -The Python release notes must mention the `ksadk-web` tag used to generate the -embedded static UI, and the release assets should include both -`ksadk-0.6.2.tar.gz` and `ksadk-0.6.2-py3-none-any.whl`. -After package publication, the publication-state check verifies that PyPI -reports package version `0.6.2`, exposes both sdist and wheel files for that -version, and links metadata to the public GitHub repository and GitHub Pages -documentation. +The Python release notes should mention the `ksadk-web` commit or tag used to +generate the embedded static UI. After package publication, the publication +state check verifies that PyPI reports the reviewed version for both public +package names. ## Credential Boundary @@ -84,7 +77,7 @@ Never commit: Before importing real source into GitHub, the approval record must name: -- the exact internal branch or commit reviewed. +- the exact private branch or commit reviewed. - whether publication uses clean export, rewritten history, or full history. - the export manifests or history scan evidence. - the reviewer who approved the sensitive-data boundary. diff --git a/public-docs/contributing/release.md b/public-docs/contributing/release.md index ca9b2b5..c1b71e7 100644 --- a/public-docs/contributing/release.md +++ b/public-docs/contributing/release.md @@ -1,19 +1,19 @@ # 发布流程 -公开发布分为内部审核、GitHub 导入、Pages、release 和 PyPI/TestPyPI。 +公开发布分为维护者审核、GitHub 导入、Pages、release 和 PyPI/TestPyPI。 ## 发布前 ```bash make open-source-review make open-source-review-bundle -python3 scripts/check_publication_state.py --phase placeholder +make public-publish-check PUBLIC_PUBLISH_PHASE=pre-publish V=0.6.4 ``` ## GitHub -审批通过后,使用审核过的 clean export 替换 placeholder。不要把完整历史直接公开, -除非历史改写和 secret scan 已单独批准。 +维护者 review 通过后,从已审核的 GitHub `main` commit 创建 tag 和 release。 +不要从未同步的候选分支直接创建公开 release。 ## PyPI @@ -23,3 +23,7 @@ PyPI metadata 指向: - Documentation: `https://kingsoftcloud.github.io/ksadk-python/` 上传凭证只放本地或发布系统 secrets,不进入 GitHub。 + +真正执行 `make publish` 或 `make publish-test` 前,必须填好 +`docs/maintainer-approval-record.md`。该记录需要包含已审核 commit SHA、 +发布策略和维护者签署。 diff --git a/public-docs/getting-started/architecture.en.md b/public-docs/getting-started/architecture.en.md new file mode 100644 index 0000000..cb4b272 --- /dev/null +++ b/public-docs/getting-started/architecture.en.md @@ -0,0 +1,63 @@ +# Architecture + +KsADK's public architecture is an Agent Runtime Platform layer. You keep building +business agents with your chosen framework, while KsADK unifies runtime, +debugging, protocols, tools, sandboxing, deployment, and observability. + +![KsADK Agent Runtime Platform architecture](../assets/ksadk-runtime-architecture.png) + +## Overview + +```text +ADK / LangGraph / LangChain / DeepAgents + │ + ▼ + KsADK + │ + ┌───────────────┼────────────────┐ + ▼ ▼ ▼ + Skill Runtime Workspace Sandbox + │ │ │ + └───────────────┼────────────────┘ + ▼ + AgentEngine + │ + ▼ + Hermes / OpenClaw Runtime +``` + +## Main Boundaries + +| Layer | Responsibility | +| --- | --- | +| Agent frameworks | business orchestration, state, tool calls, and model interaction | +| KsADK CLI | project creation, config loading, local runs, Web UI startup, packaging | +| Runner | adapt ADK, LangGraph, LangChain, and DeepAgents to one invocation contract | +| Local server | expose `/v1/responses`, `/v1/chat/completions`, and local Web UI APIs | +| Toolsets | provide Skill, Workspace, Platform, and Sandbox tool entrypoints | +| AgentEngine / Hermes / OpenClaw | remote runtime, deployment, and fuller backend execution | +| OpenTelemetry | standard tracing output for external observability systems | + +## Local Runtime Path + +When you run `agentengine run` or `agentengine web`: + +1. the CLI resolves the project directory, `.env`, and `agentengine.yaml`. +2. framework detection identifies ADK, LangGraph, LangChain, or DeepAgents. +3. the runner factory creates the matching runner. +4. the runner loads the user agent. +5. the terminal, Web UI, or OpenAI-Compatible API invokes the runner. +6. sessions, attachments, workspace files, tool calls, and tracing use the KsADK + runtime path. + +## Why This Architecture Matters + +The boundary lets teams keep their preferred agent frameworks while sharing: + +- one local command surface. +- one browser debugging experience. +- one OpenAI-Compatible API surface. +- one Skill / Workspace / Sandbox tool model. +- one deployment and observability path. + +For lower-level implementation details, see [Runtime Architecture](../guides/runtime-architecture.en.md). diff --git a/public-docs/getting-started/architecture.md b/public-docs/getting-started/architecture.md new file mode 100644 index 0000000..5581997 --- /dev/null +++ b/public-docs/getting-started/architecture.md @@ -0,0 +1,61 @@ +# 架构 + +KsADK 的公开架构可以理解为一层 Agent Runtime Platform:业务 Agent 仍由你选择的框架 +编写,KsADK 负责统一运行、调试、协议、工具、沙箱、部署和观测。 + +![KsADK Agent Runtime Platform 架构](../assets/ksadk-runtime-architecture.png) + +## 总览 + +```text +ADK / LangGraph / LangChain / DeepAgents + │ + ▼ + KsADK + │ + ┌───────────────┼────────────────┐ + ▼ ▼ ▼ + Skill Runtime Workspace Sandbox + │ │ │ + └───────────────┼────────────────┘ + ▼ + AgentEngine + │ + ▼ + Hermes / OpenClaw Runtime +``` + +## 核心边界 + +| 层 | 责任 | +| --- | --- | +| Agent 框架 | 编排业务逻辑、状态、工具调用和模型交互 | +| KsADK CLI | 创建项目、加载配置、本地运行、Web UI 启动和打包 | +| Runner | 把 ADK、LangGraph、LangChain、DeepAgents 适配到统一调用接口 | +| 本地 Server | 暴露 `/v1/responses`、`/v1/chat/completions` 和本地 Web UI API | +| Toolsets | 提供 Skill、Workspace、Platform、Sandbox 等工具入口 | +| AgentEngine / Hermes / OpenClaw | 承接远端运行、部署和更完整的 runtime backend | +| OpenTelemetry | 输出标准 tracing,接入外部观测系统 | + +## 本地运行路径 + +当你执行 `agentengine run` 或 `agentengine web`: + +1. CLI 解析项目目录、`.env` 和 `agentengine.yaml`。 +2. 框架检测器识别 ADK、LangGraph、LangChain 或 DeepAgents。 +3. Runner Factory 创建对应 Runner。 +4. Runner 加载用户 Agent。 +5. 本地终端、Web UI 或 OpenAI-Compatible API 调用 Runner。 +6. 会话、附件、workspace 文件、工具调用和 tracing 由 KsADK 统一处理。 + +## 为什么这个架构重要 + +这层边界让团队可以保留各自熟悉的 Agent 框架,同时共享: + +- 同一套本地命令。 +- 同一套浏览器调试体验。 +- 同一套 OpenAI-Compatible API。 +- 同一套 Skill / Workspace / Sandbox 工具模型。 +- 同一套部署和观测入口。 + +更详细的内部本地运行时实现见 [运行时架构](../guides/runtime-architecture.md)。 diff --git a/public-docs/getting-started/comparison.en.md b/public-docs/getting-started/comparison.en.md new file mode 100644 index 0000000..4bcb971 --- /dev/null +++ b/public-docs/getting-started/comparison.en.md @@ -0,0 +1,47 @@ +# Ecosystem Positioning + +This page is not a feature scorecard. ADK, LangGraph, OpenAI Agents SDK, VEADK, +and AgentRun each have mature capability boundaries. + +KsADK's core positioning is to add a unified runtime platform layer above those +frameworks. + +## Comparison Principle + +To avoid misleading readers, this page compares public project focus and the +complementary layer KsADK adds. It avoids simplistic yes/no feature scoring. + +| Project | Public project focus | KsADK's complementary layer | +| --- | --- | --- | +| Google ADK | Agent modeling, tools, multi-agent collaboration, Session/Memory, local runs, and Web debugging. | Run ADK agents alongside LangGraph, LangChain, and DeepAgents through one `agentengine` CLI, Web UI, local OpenAI-Compatible API, and deployment entrypoint. | +| LangGraph | Graph-state orchestration, streaming, checkpointing, human-in-the-loop workflows, and the LangChain ecosystem. | Add Skill Runtime, Workspace, Sandbox, Kingsoft Cloud AgentEngine, and deployment workflows around LangGraph projects. | +| OpenAI Agents SDK | OpenAI Responses API-native orchestration, tool calling, handoffs, guardrails, and tracing. | Expose non-OpenAI framework agents through local OpenAI-Compatible APIs, KsADK Web UI, and deployment workflows across multiple runtime backends. | +| VEADK | Agent development, A2UI/Frontend, AgentKit, VeFaaS, memory, knowledge base, built-in tools, and tracing for the Volcengine ecosystem. | Integrate Kingsoft Cloud AgentEngine, Skill, Workspace, Sandbox, Hermes/OpenClaw, and the open-source ksadk-web debugging experience. | +| AgentRun | Serverless Devs scaffolding and deployment, AgentRuntime lifecycle, OpenAI-compatible invocation, MCP/FunctionCall tools, Sandbox, knowledge base, and memory collection for Alibaba Cloud AgentRun Runtime. | Validate local multi-framework agents through one CLI, Web UI, tool, and sandbox path before connecting them to Kingsoft Cloud AgentEngine, Hermes/OpenClaw, and Skill Runtime. | + +Terms such as A2UI/Frontend, VeFaaS, Serverless Devs, and AgentRuntime lifecycle +are kept intentionally so the comparison is grounded in public project +boundaries instead of flattening real capabilities into vague yes/no cells. + +## How To Choose + +| Your goal | Recommended path | +| --- | --- | +| Build one agent quickly with one framework | Start directly with ADK, LangGraph, LangChain, or OpenAI Agents SDK | +| Add local Web UI and OpenAI-Compatible APIs to an existing LangGraph/ADK project | Wrap the project with KsADK | +| Maintain agents built with multiple frameworks | Use KsADK to unify runtime, debugging, and deployment entrypoints | +| Use Kingsoft Cloud AgentEngine, Skill, Workspace, Sandbox, or Hermes/OpenClaw | Use the KsADK runtime and deployment workflow | + +## What KsADK Does Not Do + +- it does not force you to migrate to one agent framework. +- it does not reimplement every framework capability. +- it does not use inaccurate scorecards to downplay other projects. +- it does not rewrite model output by default; if you need Markdown shape repair, + call `ksadk.markdown.repair_markdown(text, enabled=True)` in application code. + +## Continue Reading + +- [Why KsADK](why-ksadk.en.md) +- [Architecture](architecture.en.md) +- [Quick Start](quickstart.en.md) diff --git a/public-docs/getting-started/comparison.md b/public-docs/getting-started/comparison.md new file mode 100644 index 0000000..a1c15fe --- /dev/null +++ b/public-docs/getting-started/comparison.md @@ -0,0 +1,45 @@ +# 生态定位对比 + +这页不是能力打分榜。ADK、LangGraph、OpenAI Agents SDK、VEADK 和 AgentRun 都有自己成熟 +的能力边界。 + +KsADK 的核心定位是:在这些框架之上补一层统一运行时平台。 + +## 对比原则 + +为了避免误导,这里只比较公开项目的主要侧重点和 KsADK 的互补层,不用“谁有谁没有”的 +简单打分表。 + +| 项目 | 公开项目侧重点 | KsADK 更关注的互补层 | +| --- | --- | --- | +| Google ADK | Agent 建模、工具、多 Agent 协作、Session/Memory、本地运行与 Web 调试。 | 让 ADK Agent 和 LangGraph、LangChain、DeepAgents 共用同一套 `agentengine` CLI、Web UI、本地 OpenAI-Compatible API 与部署入口。 | +| LangGraph | 图状态编排、streaming、checkpoint、人机协作和 LangChain 生态。 | 为 LangGraph 项目补齐 Skill Runtime、Workspace、Sandbox、金山云 AgentEngine 和部署链路。 | +| OpenAI Agents SDK | 面向 OpenAI Responses API 的 Agent 编排、工具调用、handoff、guardrails 和 tracing。 | 面向多框架和多运行后端,把非 OpenAI 框架 Agent 也暴露为本地 OpenAI-Compatible API,并接入 KsADK Web UI 与部署工作流。 | +| VEADK | 面向火山引擎生态的 Agent 构建、A2UI/Frontend、AgentKit、VeFaaS、记忆、知识库、内置工具和 tracing。 | 面向金山云生态整合 AgentEngine、Skill、Workspace、Sandbox、Hermes/OpenClaw 和 ksadk-web 开源调试体验。 | +| AgentRun | 面向阿里云 AgentRun Runtime 的 Serverless Devs 脚手架与部署、AgentRuntime 生命周期、OpenAI-compatible 调用、MCP/FunctionCall 工具、Sandbox、知识库和记忆集合。 | 让本地多框架 Agent 先通过统一 CLI、Web UI、工具与沙箱链路跑通,再接入金山云 AgentEngine、Hermes/OpenClaw 和 Skill Runtime。 | + +这里保留 A2UI/Frontend、VeFaaS、Serverless Devs、AgentRuntime 生命周期等公开项目术语, +是为了让对比基于事实边界,而不是用模糊的“支持/不支持”压扁不同项目的真实能力。 + +## 怎么选择 + +| 你的目标 | 推荐路径 | +| --- | --- | +| 只想用一个框架快速写 Agent | 直接从 ADK、LangGraph、LangChain 或 OpenAI Agents SDK 开始 | +| 已经有 LangGraph/ADK 项目,需要本地 Web UI 和 OpenAI-Compatible API | 在项目外层接入 KsADK | +| 团队同时维护多个框架的 Agent | 用 KsADK 统一运行、调试和部署入口 | +| 需要金山云 AgentEngine、Skill、Workspace、Sandbox 或 Hermes/OpenClaw | 使用 KsADK 的 runtime 和部署链路 | + +## KsADK 不做什么 + +- 不强迫你迁移到某一个 Agent 框架。 +- 不把所有框架能力重新实现一遍。 +- 不用不准确的功能打分表贬低其他项目。 +- 不默认改写模型输出;如需 Markdown 形态修复,可在业务侧显式调用 + `ksadk.markdown.repair_markdown(text, enabled=True)`。 + +## 继续阅读 + +- [为什么需要 KsADK](why-ksadk.md) +- [架构](architecture.md) +- [快速开始](quickstart.md) diff --git a/public-docs/getting-started/configuration.en.md b/public-docs/getting-started/configuration.en.md index 7467b00..5c4daca 100644 --- a/public-docs/getting-started/configuration.en.md +++ b/public-docs/getting-started/configuration.en.md @@ -71,7 +71,7 @@ Common fields: | `framework` | framework adapter | `adk`, `langchain`, `langgraph`, `deepagents` | | `entry_point` | Python file loaded by the local runtime | `agent.py` | | `agent_variable` | exported object name | `root_agent` | -| `region` | optional cloud region for deployment-shaped commands | `cn-example-1` | +| `region` | optional cloud region for deployment-shaped commands | `cn-beijing-6` | Prefer explicit project YAML in public samples. It is easier for contributors to review than relying on framework auto-detection. diff --git a/public-docs/getting-started/configuration.md b/public-docs/getting-started/configuration.md index 9001725..e95ac48 100644 --- a/public-docs/getting-started/configuration.md +++ b/public-docs/getting-started/configuration.md @@ -63,7 +63,7 @@ agent_variable: root_agent | `framework` | 框架适配器 | `adk`、`langchain`、`langgraph`、`deepagents` | | `entry_point` | 本地运行时加载的 Python 文件 | `agent.py` | | `agent_variable` | 导出的对象名 | `root_agent` | -| `region` | 部署形态命令使用的可选云 region | `cn-example-1` | +| `region` | 部署形态命令使用的可选云 region | `cn-beijing-6` | 公开示例优先写显式项目 YAML。它比依赖自动检测更容易审核。 diff --git a/public-docs/getting-started/why-ksadk.en.md b/public-docs/getting-started/why-ksadk.en.md new file mode 100644 index 0000000..6e28d12 --- /dev/null +++ b/public-docs/getting-started/why-ksadk.en.md @@ -0,0 +1,86 @@ +# Why KsADK + +KsADK is not another agent framework. It is a unified runtime layer for agents +built with existing frameworks. + +Most frameworks answer: + +```text +How do I build an agent? +``` + +KsADK focuses on: + +```text +How do I run, debug, expose, deploy, and observe agents consistently? +``` + +## One-line Positioning + +Build agents once. Run them anywhere. + +KsADK is the Agent Runtime Platform for AI agents. + +Keep building business logic with Google ADK, LangGraph, LangChain, or +DeepAgents. Use KsADK for one CLI, local Web UI, OpenAI-Compatible API, Skill +Runtime, Workspace, Sandbox, deployment, and OpenTelemetry observability path. + +## Why Not Just ADK + +Google ADK focuses on agent modeling, tools, multi-agent collaboration, +Session/Memory, local runs, and Web debugging. + +KsADK does not replace ADK. KsADK lets ADK agents share the same local runtime, +browser debugging UI, OpenAI-Compatible API, and deployment entrypoint as +LangGraph, LangChain, and DeepAgents projects. + +KsADK is useful when: + +- your team has both ADK and LangGraph projects. +- you want one Web UI to debug agents built with different frameworks. +- you need to expose local agents through OpenAI-Compatible APIs. +- you need Kingsoft Cloud AgentEngine, Skill, Workspace, Sandbox, or deployment + integration. + +## Why Not Just LangGraph + +LangGraph is strong at graph-state orchestration, checkpointing, streaming, +human-in-the-loop workflows, and the LangChain ecosystem. + +KsADK does not rewrite LangGraph execution. It adds a runtime platform layer +around LangGraph projects: + +- `agentengine run` for local terminal interaction. +- `agentengine web` for browser debugging. +- `/v1/responses` and `/v1/chat/completions` local protocols. +- Skill Runtime, Workspace, and Sandbox toolsets. +- AgentEngine, Hermes, OpenClaw, and Serverless deployment entrypoints. + +## Why Not Just OpenAI Agents SDK + +OpenAI Agents SDK is native to the OpenAI Responses API and provides agent +orchestration, tool calling, handoffs, guardrails, and tracing. + +KsADK is multi-framework and multi-runtime. It does not require your business +agent to be written with the OpenAI Agents SDK. It wraps ADK, LangGraph, +LangChain, and DeepAgents projects into one local runtime, debugging, +OpenAI-Compatible API, and deployment workflow. + +## What KsADK Solves + +| Problem | KsADK capability | +| --- | --- | +| Start a new agent project quickly | `agentengine init`, `agentengine config`, `agentengine run` | +| Debug streaming, attachments, and tool calls in a browser | `agentengine web` and the ksadk-web debugging UI | +| Use one invocation protocol across frameworks | `/v1/responses`, `/v1/chat/completions` | +| Add tools, workspace access, and isolated execution | Skill Runtime, Workspace tools, Sandbox tools | +| Deploy to remote runtimes | `agentengine build`, `agentengine launch`, Hermes/OpenClaw | +| Observe consistently | OpenTelemetry / OTLP tracing | + +## When You May Not Need KsADK + +If you only have a single-file script that directly calls a model API and you do +not need browser debugging, isolated tools, unified protocols, deployment, or +observability, the framework itself may be enough. + +KsADK becomes useful when you need one way to manage multiple agent projects. diff --git a/public-docs/getting-started/why-ksadk.md b/public-docs/getting-started/why-ksadk.md new file mode 100644 index 0000000..5927561 --- /dev/null +++ b/public-docs/getting-started/why-ksadk.md @@ -0,0 +1,81 @@ +# 为什么需要 KsADK + +KsADK 的定位不是再造一个 Agent 框架,而是给已有 Agent 框架补齐统一运行时。 + +大多数框架回答的是: + +```text +如何编写 Agent? +``` + +KsADK 更关注: + +```text +如何把 Agent 跑起来、调起来、接出去、部署出去,并持续观测? +``` + +## 一句话定位 + +一次构建 Agent,到处运行。 + +KsADK 是面向 AI Agent 的运行时平台(Agent Runtime Platform)。 + +你可以继续使用 Google ADK、LangGraph、LangChain 或 DeepAgents 编写业务逻辑,再用 +KsADK 获得统一的 CLI、本地 Web UI、OpenAI-Compatible API、Skill Runtime、 +Workspace、Sandbox、部署和 OpenTelemetry 观测能力。 + +## 为什么不是 ADK + +Google ADK 主要解决 Agent 建模、工具、多 Agent 协作、Session/Memory、本地运行与 +Web 调试。 + +ADK 解决 Agent 开发;KsADK 解决 Agent 运行。 + +KsADK 不替代 ADK。KsADK 的目标是让 ADK Agent 可以和 LangGraph、LangChain、 +DeepAgents 项目共用同一套本地运行、浏览器调试、OpenAI-Compatible API 和部署入口。 + +适合使用 KsADK 的场景: + +- 团队里同时有 ADK 和 LangGraph 项目。 +- 需要用同一套 Web UI 调试不同框架的 Agent。 +- 需要把本地 Agent 暴露成 OpenAI-Compatible API。 +- 需要接入金山云 AgentEngine、Skill、Workspace、Sandbox 或部署链路。 + +## 为什么不是 LangGraph + +LangGraph 擅长图状态编排、checkpoint、streaming、人机协作和 LangChain 生态。 + +KsADK 不重写 LangGraph 的图执行能力。KsADK 在 LangGraph 项目之外补上运行时平台层: + +- `agentengine run` 本地交互。 +- `agentengine web` 浏览器调试。 +- `/v1/responses` 和 `/v1/chat/completions` 本地协议。 +- Skill Runtime、Workspace 和 Sandbox toolsets。 +- AgentEngine、Hermes、OpenClaw 和 Serverless 部署入口。 + +## 为什么不是 OpenAI Agents SDK + +OpenAI Agents SDK 面向 OpenAI Responses API,提供 Agent 编排、工具调用、handoff、 +guardrails 和 tracing。 + +KsADK 面向多框架和多运行后端。它不会要求你的业务 Agent 一定采用 OpenAI Agents SDK +编写,而是把 ADK、LangGraph、LangChain 和 DeepAgents 项目统一包装到本地运行、调试、 +OpenAI-Compatible API 和部署链路里。 + +## KsADK 解决什么问题 + +| 问题 | KsADK 提供的能力 | +| --- | --- | +| 新 Agent 项目如何快速跑起来 | `agentengine init`、`agentengine config`、`agentengine run` | +| 如何用浏览器调试 streaming、附件和工具调用 | `agentengine web` 和 ksadk-web 调试界面 | +| 如何让不同框架使用同一套调用协议 | `/v1/responses`、`/v1/chat/completions` | +| 如何给 Agent 接入工具、workspace 和隔离执行 | Skill Runtime、Workspace tools、Sandbox tools | +| 如何部署到远端运行时 | `agentengine build`、`agentengine launch`、Hermes/OpenClaw | +| 如何统一观测 | OpenTelemetry / OTLP tracing | + +## 什么时候不需要 KsADK + +如果你只是写一个单文件脚本,直接调用模型 API,没有浏览器调试、工具隔离、统一协议、 +部署和观测需求,直接使用框架本身就够了。 + +当你开始需要“同一套方式管理多个 Agent 项目”时,KsADK 的价值才会变明显。 diff --git a/public-docs/guides/agent-best-practices.en.md b/public-docs/guides/agent-best-practices.en.md index 4ef60e7..b1d64b3 100644 --- a/public-docs/guides/agent-best-practices.en.md +++ b/public-docs/guides/agent-best-practices.en.md @@ -119,6 +119,42 @@ workflow.add_edge("finalize_answer", END) root_agent = workflow.compile(name="production_agent") ``` +## Markdown Output Repair + +KsADK preserves raw model output by default. The runtime does not normalize +Markdown on the backend, so streaming, tracing, session replay, and audit logs +all see the same raw LLM output. + +If your application exports answers as reports, message cards, knowledge-base +documents, or workspace Markdown files, enable the lightweight repair helper in +your own business code: + +```python +from ksadk.markdown import repair_markdown + + +def finalize_answer(state: AgentState) -> dict[str, Any]: + text = "" + for message in reversed(state.get("specialist_messages") or []): + if isinstance(message, AIMessage) and message.content: + text = str(message.content) + break + + # App-side opt-in. KsADK runtime does not rewrite raw model output by default. + text = repair_markdown(text, enabled=True) + return {"final_text": text, "messages": [AIMessage(content=text)]} +``` + +`repair_markdown()` only performs conservative shape repairs: + +- closes unclosed fenced code blocks. +- inserts missing blank lines around code blocks, lists, and tables. +- normalizes newlines and remains idempotent. + +It does not rewrite semantics, validate facts, or guarantee strict CommonMark. +For strict output contracts, use JSON schema, Pydantic validation, retries, or +application-level linting before export. + `agentengine.yaml` stays explicit: ```yaml diff --git a/public-docs/guides/agent-best-practices.md b/public-docs/guides/agent-best-practices.md index a1532d8..f9e4f0a 100644 --- a/public-docs/guides/agent-best-practices.md +++ b/public-docs/guides/agent-best-practices.md @@ -115,6 +115,39 @@ workflow.add_edge("finalize_answer", END) root_agent = workflow.compile(name="production_agent") ``` +## Markdown 输出修复 + +KsADK runtime 默认保留模型原文,不会在后端自动规范化 Markdown。这样可以保证 +streaming、trace、会话回放和业务审计看到的是同一份 raw LLM output。 + +如果你的业务要把回答导出为报告、消息卡片、知识库文档或 workspace Markdown +文件,可以在业务代码里显式开启轻量修复: + +```python +from ksadk.markdown import repair_markdown + + +def finalize_answer(state: AgentState) -> dict[str, Any]: + text = "" + for message in reversed(state.get("specialist_messages") or []): + if isinstance(message, AIMessage) and message.content: + text = str(message.content) + break + + # 业务侧按需开启。KsADK runtime 默认不会改写模型原文。 + text = repair_markdown(text, enabled=True) + return {"final_text": text, "messages": [AIMessage(content=text)]} +``` + +`repair_markdown()` 只做保守形态修复: + +- 补齐未闭合 fenced code block。 +- 给代码块、列表和表格补必要空行。 +- 统一换行并保持幂等。 + +它不会重写语义、不会替你校验事实,也不会保证严格 CommonMark。强格式场景仍建议 +使用 JSON schema、Pydantic 校验、retry 或业务侧导出前 lint。 + `agentengine.yaml` 建议显式声明: ```yaml diff --git a/public-docs/guides/build-and-package.en.md b/public-docs/guides/build-and-package.en.md index f2e23e9..b16ae0a 100644 --- a/public-docs/guides/build-and-package.en.md +++ b/public-docs/guides/build-and-package.en.md @@ -78,19 +78,19 @@ Release artifacts must not contain: The Python package may include static UI assets needed by `agentengine web`. Editable UI source belongs to `ksadk-web`. -## Internal Review Before Public Push +## Maintainer Review Before Public Push The intended publication sequence is: -1. push the review branch to internal ezone. -2. complete internal maintainer review. +1. push the review branch to the private maintainer review channel. +2. complete maintainer review. 3. import reviewed source into `kingsoftcloud/ksadk-python`. 4. import reviewed UI source into `kingsoftcloud/ksadk-web`. 5. enable GitHub Pages. 6. create GitHub releases. 7. publish PyPI or TestPyPI packages. -Do not skip the internal review step. PyPI credentials, TestPyPI credentials, +Do not skip the maintainer review step. PyPI credentials, TestPyPI credentials, and release tokens must stay outside GitHub source. ## Clean Export Versus Full History diff --git a/public-docs/guides/local-web-ui.md b/public-docs/guides/local-web-ui.md index 3357e39..043d0f0 100644 --- a/public-docs/guides/local-web-ui.md +++ b/public-docs/guides/local-web-ui.md @@ -42,7 +42,7 @@ agentengine web . --model my-model ## 与运行时的关系 -UI 调用本地 KsADK 运行时。普通 SDK 用户安装 wheel 后已经拿到静态 UI 资源。 +UI 调用本地 KsADK 运行时。通过 pip 安装 KsADK wheel 后已经拿到静态 UI 资源。 只有开发 UI 源码时才需要 Node.js。 ```mermaid diff --git a/public-docs/guides/runtime-products.en.md b/public-docs/guides/runtime-products.en.md index 842568e..e11a6ba 100644 --- a/public-docs/guides/runtime-products.en.md +++ b/public-docs/guides/runtime-products.en.md @@ -136,7 +136,7 @@ placeholders, not real values. Do not publish: - private runtime image names or registries. -- internal pre-release endpoints. +- private-environment endpoints. - kubeconfig files or cluster names. - real channel tokens, model keys, or kdocs tokens. - customer workspace paths or object storage buckets. @@ -144,4 +144,3 @@ Do not publish: The public docs should explain the contract and lifecycle. Operational details for a specific private environment belong in internal documentation. - diff --git a/public-docs/guides/runtime-products.md b/public-docs/guides/runtime-products.md index 9625025..c0f9f56 100644 --- a/public-docs/guides/runtime-products.md +++ b/public-docs/guides/runtime-products.md @@ -131,11 +131,10 @@ Hermes 和 OpenClaw 可能从环境变量读取模型、记忆、channel 或工 不要发布: - 私有 runtime 镜像名或 registry。 -- 内部预发 endpoint。 +- 私有环境 endpoint。 - kubeconfig 文件或集群名称。 - 真实 channel token、模型 key 或 kdocs token。 - 客户 workspace 路径或对象存储 bucket。 - 绑定内部平台的事故 runbook。 公开文档解释 contract 和生命周期。具体私有环境的运维细节留在内部文档。 - diff --git a/public-docs/index.en.md b/public-docs/index.en.md index 48096c5..3a84170 100644 --- a/public-docs/index.en.md +++ b/public-docs/index.en.md @@ -1,180 +1,184 @@ # KsADK -Kingsoft Cloud Agent Development Kit for building, running, debugging, and -packaging Python agent applications. KsADK gives developers one local CLI, -runtime surface, OpenAI-compatible protocol layer, and browser UI across Google -ADK, LangGraph, LangChain, and DeepAgents projects. +Build agents once. Run them anywhere. -=== "Python" +KsADK is the Agent Runtime Platform for AI agents. + +Build with Google ADK, LangGraph, LangChain, or DeepAgents. Run, debug, expose, observe, and deploy those agents through one unified runtime experience. + +![Real KsADK CLI screenshot: agentengine -h](assets/ksadk-runtime-platform-hero-wide.png){ width="920" } + +=== "Install" ```bash - pip install -U ksadk - pip install -U "ksadk[langgraph]" + pip install -U "ksadk[all]" ``` === "Create" ```bash - agentengine init my-agent -f langgraph - cd my-agent - agentengine config set OPENAI_API_KEY=sk-test OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_MODEL_NAME=my-model + agentengine init demo-agent -f langgraph + cd demo-agent + agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini ``` === "Run" ```bash - agentengine run . -i + agentengine run -i agentengine web . --no-open ``` -This site is the curated public documentation for the open-source SDK. It is -separate from generated `.zread/` code-reading output, internal deployment -notes, and private AgentEngine operating procedures. - -## System At A Glance - -```mermaid -flowchart LR - Dev["Developer"] --> CLI["agentengine CLI"] - CLI --> Detect["project detection
agentengine.yaml / ksadk.yaml / conventions"] - Detect --> Runner["framework runner
ADK / LangGraph / LangChain / DeepAgents"] - Runner --> Runtime["local runtime"] - Runtime --> API["OpenAI-compatible APIs
/v1/responses
/v1/chat/completions"] - Runtime --> UI["local Web UI
agentengine web"] - Runtime --> Sessions["sessions, attachments,
workspace files, tracing"] - CLI --> Package["build / launch
reviewed cloud path"] - UI -. "editable source" .-> Web["kingsoftcloud/ksadk-web"] +## Why KsADK + +Most agent frameworks solve agent development. + +KsADK solves agent runtime. + +KsADK does not replace your agent framework. It provides a unified platform layer for development, debugging, runtime, sandbox, deployment, and observability: + +- Development: one CLI for project creation, configuration, and local runs. +- Debugging: browser UI, sessions, attachments, workspace files, and streaming. +- Runtime: framework runners, OpenAI-Compatible APIs, and consistent invocation. +- Sandbox: Skill Runtime, Workspace, and isolated sandbox backend boundaries. +- Deployment: Serverless, Hermes, OpenClaw, and remote AgentEngine entrypoints. +- Observability: OpenTelemetry-first tracing for multiple backends. + +## 30 Seconds Quick Start + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -U "ksadk[all]" + +agentengine init demo-agent -f langgraph +cd demo-agent +agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini +agentengine run -i +``` + +Open the local Web UI: + +```bash +agentengine web . --no-open ``` -The important design choice is that the SDK does not replace the framework -where the agent is written. It detects and loads the project, adapts it through -a runner, then exposes the same local development experience to terminal users, -browser users, and API clients. - -## Documentation Style - -KsADK follows the public-docs pattern used by mature agent SDK projects: - -- a short overview for positioning. -- a quickstart that reaches a running local agent. -- tutorials with complete files. -- guides for common tasks and operational decisions. -- references for command, config, and API contracts. -- contribution, release, security, and publication gates. - -Generated code-reading output can help maintainers understand the repository, -but it is not the public documentation source. Public docs must be reviewed, -stable, linkable, and safe to publish on GitHub Pages. - -## Developer Journey - -```mermaid -flowchart TD - A["Install ksadk"] --> B["Create or import an agent project"] - B --> C["Set model/provider configuration"] - C --> D["Run in terminal"] - D --> E["Debug in local Web UI"] - E --> F["Call local OpenAI-compatible APIs"] - F --> G["Add tools, files, memory, tracing"] - G --> H["Build and review release artifacts"] - H --> I["Publish only after maintainer approval"] +The demo below is generated from the real local Web UI. It uses a deterministic LangGraph Runner and does not call an external model or cloud service, while still exercising local FastAPI, Responses streaming, tool calls, thinking output, and session status. + +![Real KsADK Web UI debugging screenshot](assets/ksadk-web-ui-screenshot.png){ width="920" } + +![Real KsADK Web UI debugging GIF](assets/ksadk-local-debugging-demo.gif){ width="920" } + +If your model provider is not the default OpenAI endpoint, also set: + +```bash +agentengine config set OPENAI_BASE_URL=https://api.example.com/v1 ``` -## What KsADK Provides +If you need Kingsoft Cloud AgentEngine, Skill Service, knowledge base, or long-term memory services, set the default public cloud region explicitly: -| Area | What you get | +```bash +agentengine config set KSYUN_REGION=cn-beijing-6 +``` + +## Architecture + +![KsADK Agent Runtime Platform architecture](assets/ksadk-runtime-architecture.png){ width="920" } + +This diagram shows the public runtime boundary: keep building business agents with ADK, LangGraph, LangChain, or DeepAgents, then use KsADK for one CLI, browser Web UI, OpenAI-Compatible APIs, Skill Runtime, Workspace, Sandbox, memory, knowledge, and deployment backends. + +## Supported Frameworks + +| Framework | What KsADK adds | | --- | --- | -| Project bootstrap | `agentengine init` templates for supported framework families. | -| Local runtime | `agentengine run` starts a local API server for an agent project. | -| Local Web UI | `agentengine web` opens a browser-based invoke/debug interface. | -| Configuration | `agentengine config` manages project `.env` and YAML settings. | -| Packaging | `agentengine build` prepares deployment artifacts when cloud credentials are configured. | -| Protocols | Local OpenAI-compatible `/v1/responses` and `/v1/chat/completions` endpoints. | -| Extensibility | Framework adapters, memory hooks, MCP/A2A integration points, and release tooling. | +| Google ADK | Templates, runner adapter, local runtime, Web UI debugging, and deployment entrypoints. | +| LangGraph | Graph-state entrypoint, tool calling, streaming, Skill Runtime, and workspace toolsets. | +| LangChain | Runnable/chain adaptation, local OpenAI-Compatible APIs, and tracing. | +| DeepAgents | Project entrypoint, runtime wrapping, browser debugging, and deployment artifacts. | -## Typical Use Cases +## Ecosystem Positioning -Use KsADK when you need to: +KsADK does not use a simplistic feature scorecard against ADK, LangGraph, the +OpenAI Agents SDK, VEADK, or AgentRun. Those projects each have mature +capability boundaries. KsADK focuses on the complementary runtime platform +layer: -- run the same local command against ADK, LangGraph, LangChain, or DeepAgents - projects. -- expose a local OpenAI-compatible endpoint for an agent project. -- debug an agent in a browser without setting up hosted infrastructure. -- prepare an agent package for a reviewed cloud deployment path. -- keep Python SDK docs, Web UI docs, and release checks aligned before a public - GitHub import. +- Agent frameworks keep owning business orchestration, state, and model interaction. +- KsADK unifies the CLI, Web UI, local OpenAI-Compatible APIs, tools, sandbox, + deployment, and observability. +- Kingsoft Cloud AgentEngine, Skill, Workspace, Sandbox, Hermes, and OpenClaw + are exposed through one developer entrypoint. -## Open-Source Boundary +For details, see [Ecosystem Positioning](getting-started/comparison.en.md). -The public repository contains the SDK, CLI, runtime adapters, local development -experience, curated documentation, and release checks. +## Core Capabilities -It does not publish the full AgentEngine control plane, internal Kubernetes -deployment automation, internal kubeconfig material, private registries, customer -data, or private support runbooks. Cloud deployment commands are documented as -SDK entry points, but public examples must be runnable locally without internal -accounts. +| Capability | Common entrypoints | +| --- | --- | +| Local Development | `agentengine init`, `agentengine config`, `agentengine run` | +| Browser Debugging UI | `agentengine web` | +| OpenAI-Compatible API | `/v1/responses`, `/v1/chat/completions` | +| Unified Runtime | ADK / LangGraph / LangChain / DeepAgents runners | +| Sandbox Execution | Skill Runtime, Workspace tools, Sandbox tools | +| Serverless Deployment | `agentengine build`, `agentengine launch` | +| Hermes & OpenClaw Runtime | `agentengine hermes ...`, `agentengine openclaw ...` | -## First Workflow +## Examples -```bash -python -m venv .venv -source .venv/bin/activate -pip install -U ksadk +The public samples repository is organized by scenario, not only by framework: -agentengine init my-agent -f langgraph -cd my-agent -agentengine config set OPENAI_API_KEY=sk-test OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_MODEL_NAME=my-model -agentengine run -i -``` +- [KSADK Samples](https://github.com/kingsoftcloud/ksadk-samples) +- Knowledge Assistant: RAG and knowledge-base QA. +- Workflow Agent: LangGraph plus AgentEngine toolsets. +- Tool-Using Agent: custom business tools. +- Memory-aware Agent: short-term and long-term memory patterns. + +## Deployment -Then open the local Web UI: +KsADK is local-first, with reviewed deployment entrypoints when you are ready: ```bash -agentengine web . --no-open +agentengine build . +agentengine launch . --target serverless +agentengine dashboard open ``` -If you already have an agent file, use: +When updating existing Hermes or OpenClaw instances, KsADK preserves server-side env, storage, network, and memory configuration by default. Those groups are overwritten only when matching CLI options are provided explicitly. + +## Observability + +KsADK is OpenTelemetry-native. ```bash -agentengine init my-agent --from-agent ./agent.py -cd my-agent -agentengine run . -i +OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com +OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20token ``` -## Documentation Map - -- [Concepts](getting-started/concepts.en.md): how the SDK, project config, runtime, and Web UI fit together. -- [Quickstart](getting-started/quickstart.en.md): create, configure, run, and debug a local agent. -- [Configuration](getting-started/configuration.en.md): environment variables and project YAML. -- [Project Structure](getting-started/project-structure.en.md): files created by templates and files to avoid committing. -- [Build A LangGraph Agent](tutorials/langgraph-agent.en.md): complete local project with source code. -- [Bring An Existing Agent](tutorials/existing-agent.en.md): wrap an existing file or package. -- [Local Web UI](guides/local-web-ui.en.md): local browser debugging and the independent `ksadk-web` repository plan. -- [Web UI Repository](guides/web-ui-source.en.md): how `ksadk-web`, hosted UI, and the Python wheel relate. -- [Runtime Products](guides/runtime-products.en.md): Hermes and OpenClaw lifecycle, runtime surfaces, and public-safe boundaries. -- [Frameworks](guides/frameworks.en.md): ADK, LangGraph, LangChain, and DeepAgents conventions. -- [Agent Best Practices](guides/agent-best-practices.en.md): LangGraph/ADK patterns with knowledge, memory, sessions, Skill Runtime, MCP, and workspace files. -- [Tools And Skill Runtime](guides/tools-and-skill-runtime.en.md): built-in toolsets, focused/dispatcher progressive disclosure, Tool Gateway, MCP/A2A, and Skill Runtime boundaries. -- [Observability And Tracing](guides/observability-tracing.en.md): local spans, Langfuse, OTLP, and trace metadata rules. -- [Build And Package](guides/build-and-package.en.md): local builds, review gates, and public artifact rules. -- [CLI Reference](reference/cli.en.md): public command surface and common options. -- [OpenAI-Compatible API](reference/openai-compatible-api.en.md): local protocol shape and KsADK extensions. -- [Project Configuration](reference/project-config.en.md): YAML and environment field reference. -- [Remote Runtime API](reference/remote-runtime-api.en.md): PublicEndpoint, authentication, OpenAI-compatible routes, workspace files, Hermes, and OpenClaw. -- [Environment Variables](reference/environment-variables.en.md): model, session, memory, knowledge, Skill Runtime, MCP, build, and tracing variables. -- [Runtime Sessions And Files](reference/runtime-sessions-files.en.md): session IDs, uploads, workspace previews, and local state. -- [Security Boundaries](reference/security-boundaries.en.md): public repository, workspace, preview, package, and history boundaries. -- [Troubleshooting](reference/troubleshooting.en.md): common setup, runtime, and packaging failures. - -## Publication State - -The planned public locations are: - -- Python SDK repository: `https://github.com/kingsoftcloud/ksadk-python` -- Python SDK docs: `https://kingsoftcloud.github.io/ksadk-python/` -- Web UI repository: `https://github.com/kingsoftcloud/ksadk-web` -- Web UI docs or demo: `https://kingsoftcloud.github.io/ksadk-web/` - -The first real source import must be reviewed internally before GitHub source, -GitHub Pages, GitHub releases, or PyPI publication are enabled. +Compatible with: + +- Langfuse +- Arize +- Datadog +- Grafana +- Phoenix + +Export once. Observe anywhere. + +## Documentation + +- [Getting Started](getting-started/quickstart.en.md) +- [Build](tutorials/langgraph-agent.en.md) +- [Run](guides/local-web-ui.en.md) +- [Deploy](guides/build-and-package.en.md) +- [Observe](guides/observability-tracing.en.md) +- [Extend](guides/tools-and-skill-runtime.en.md) +- [Reference](reference/cli.en.md) + +## Community + +- Repository: +- Wiki: +- Samples repository: +- Web UI repository: +- PyPI: +- License: Apache-2.0 diff --git a/public-docs/index.md b/public-docs/index.md index 46b034a..97708f6 100644 --- a/public-docs/index.md +++ b/public-docs/index.md @@ -1,171 +1,180 @@ # KsADK -金山云智能体开发套件,用于创建、运行、调试和打包 Python Agent 应用。 -KsADK 在 Google ADK、LangGraph、LangChain 和 DeepAgents 项目之上提供 -统一的本地 CLI、运行时、OpenAI 兼容协议层和浏览器调试界面。 +一次构建 Agent,到处运行。 -=== "Python" +KsADK 是面向 AI Agent 的运行时平台(Agent Runtime Platform)。 + +你可以继续使用 Google ADK、LangGraph、LangChain 或 DeepAgents 编写业务 Agent,再用 KsADK 获得统一的本地运行、浏览器调试、OpenAI-Compatible API、沙箱执行、部署和可观测体验。 + +![KsADK 真实 CLI 截图:agentengine -h](assets/ksadk-runtime-platform-hero-wide.png){ width="920" } + +=== "安装" ```bash - pip install -U ksadk - pip install -U "ksadk[langgraph]" + pip install -U "ksadk[all]" ``` -=== "创建项目" +=== "创建" ```bash - agentengine init my-agent -f langgraph - cd my-agent - agentengine config set OPENAI_API_KEY=sk-test OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_MODEL_NAME=my-model + agentengine init demo-agent -f langgraph + cd demo-agent + agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini ``` -=== "本地运行" +=== "运行" ```bash - agentengine run . -i + agentengine run -i agentengine web . --no-open ``` -本站是开源 SDK 的人工整理公开文档。它不发布 `.zread/` 代码阅读结果、 -内部部署说明或私有 AgentEngine 运维流程。 - -## 架构一览 - -```mermaid -flowchart LR - Dev["开发者"] --> CLI["agentengine CLI"] - CLI --> Detect["项目检测
agentengine.yaml / ksadk.yaml / 约定"] - Detect --> Runner["框架 Runner
ADK / LangGraph / LangChain / DeepAgents"] - Runner --> Runtime["本地运行时"] - Runtime --> API["OpenAI 兼容 API
/v1/responses
/v1/chat/completions"] - Runtime --> UI["本地 Web UI
agentengine web"] - Runtime --> Sessions["会话、附件、
工作区文件、Tracing"] - CLI --> Package["build / launch
审核后的云端路径"] - UI -. "可编辑源代码" .-> Web["kingsoftcloud/ksadk-web"] +## 为什么需要 KsADK + +大多数 Agent 框架主要解决“如何开发 Agent”。 + +KsADK 解决“如何运行、调试、部署和观测 Agent”。 + +KsADK 不替换你已经选择的 Agent 框架。它在框架之上提供统一平台层,把开发、调试、运行、沙箱、部署和可观测连接起来: + +- 开发:统一项目创建、配置和本地运行。 +- 调试:浏览器调试 UI、会话、附件、workspace 文件和 streaming。 +- 运行:统一 Runner、OpenAI-Compatible API 和多框架入口。 +- Sandbox:Skill Runtime、Workspace 和 sandbox backend 的隔离执行边界。 +- 部署:Serverless、Hermes、OpenClaw 和远端 AgentEngine 入口。 +- 可观测:OpenTelemetry-first tracing,可接入多种观测后端。 + +## 30 秒快速体验 + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -U "ksadk[all]" + +agentengine init demo-agent -f langgraph +cd demo-agent +agentengine config set OPENAI_API_KEY=your-api-key OPENAI_MODEL_NAME=gpt-4o-mini +agentengine run -i ``` -这个设计的核心是:KsADK 不替换你写 Agent 时使用的框架。它负责发现项目、 -加载入口、适配 Runner,并把同一套本地开发体验提供给终端、浏览器和 API -客户端。 +启动本地 Web UI: -## 文档定位 +```bash +agentengine web . --no-open +``` -KsADK 参考成熟 Agent SDK 项目的公开文档组织方式: +下面是脚本生成的真实本地 Web UI 演示:使用 deterministic LangGraph Runner,不连接外部模型或云环境,但完整走本地 FastAPI、Responses streaming、工具调用、思考过程和会话状态链路。 -- 概览页说明定位和主要入口。 -- 快速开始把用户带到一个可以运行的本地 Agent。 -- 教程给出完整文件。 -- 指南解释常见任务和取舍。 -- 参考页定义命令、配置和 API 契约。 -- 贡献、发布、安全和公开审计规则作为治理文档。 +![KsADK 真实 Web UI 调试截图](assets/ksadk-web-ui-screenshot.png){ width="920" } -zread 生成的代码阅读结果可以帮助维护者理解仓库,但不是公开文档源。公开文档 -必须经过人工整理、可链接、可审核,并且可以安全发布到 GitHub Pages。 +![KsADK 真实 Web UI 调试 GIF](assets/ksadk-local-debugging-demo.gif){ width="920" } -## 开发路径 +如果使用非默认 OpenAI endpoint,再额外设置: -```mermaid -flowchart TD - A["安装 ksadk"] --> B["创建或导入 Agent 项目"] - B --> C["配置模型与 provider"] - C --> D["终端交互运行"] - D --> E["本地 Web UI 调试"] - E --> F["调用本地 OpenAI 兼容 API"] - F --> G["添加工具、文件、记忆和追踪"] - G --> H["构建并审核发布制品"] - H --> I["维护者批准后再公开发布"] +```bash +agentengine config set OPENAI_BASE_URL=https://api.example.com/v1 ``` -## KsADK 提供什么 +如果需要调用金山云 AgentEngine、Skill Service、知识库或长期记忆等线上能力,建议显式设置线上默认地域: + +```bash +agentengine config set KSYUN_REGION=cn-beijing-6 +``` -| 能力区域 | 你会得到什么 | +## 架构 + +![KsADK Agent Runtime Platform 架构](assets/ksadk-runtime-architecture.png){ width="920" } + +这张图展示的是公开运行时边界:业务 Agent 仍然由 ADK、LangGraph、LangChain 或 DeepAgents 编写;KsADK 在上层补齐统一 CLI、Web UI、OpenAI-Compatible API、Skill Runtime、Workspace、Sandbox、记忆、知识库和部署后端。 + +## 支持的框架 + +| 框架 | KsADK 负责什么 | | --- | --- | -| 项目脚手架 | 面向不同框架族的 `agentengine init` 模板。 | -| 本地运行时 | `agentengine run` 为 Agent 项目启动本地 API 服务。 | -| 本地 Web UI | `agentengine web` 打开浏览器调用和调试界面。 | -| 配置管理 | `agentengine config` 管理项目 `.env` 与 YAML 设置。 | -| 打包 | 在配置云凭证后,`agentengine build` 准备部署制品。 | -| 协议 | 本地 OpenAI 兼容 `/v1/responses` 与 `/v1/chat/completions`。 | -| 扩展能力 | 框架适配、记忆 hook、MCP/A2A 接入点和发布工具链。 | +| Google ADK | 项目模板、Runner 适配、本地运行、Web UI 调试和部署入口。 | +| LangGraph | 图状态入口、工具调用、streaming、Skill Runtime 和 workspace toolsets。 | +| LangChain | Runnable/chain 适配、本地 OpenAI-Compatible API 和 tracing。 | +| DeepAgents | 项目入口、运行时包装、浏览器调试和部署制品。 | -## 典型场景 +## 生态定位 -适合使用 KsADK 的情况: +KsADK 不和 ADK、LangGraph、OpenAI Agents SDK、VEADK 或 AgentRun 做简单功能打分。 +这些项目各有成熟能力,KsADK 更关注互补的运行时平台层: -- 希望用同一组本地命令运行 ADK、LangGraph、LangChain 或 DeepAgents 项目。 -- 希望为 Agent 项目暴露本地 OpenAI 兼容 endpoint。 -- 希望不搭建 hosted 基础设施就能在浏览器里调试 Agent。 -- 希望为经过审核的云端部署路径准备 Agent 包。 -- 希望 Python SDK 文档、Web UI 文档和发布检查在公开 GitHub 导入前保持一致。 +- 已有 Agent 框架继续负责业务编排、状态和模型交互。 +- KsADK 负责统一 CLI、Web UI、本地 OpenAI-Compatible API、工具、沙箱、部署和可观测。 +- 金山云 AgentEngine、Skill、Workspace、Sandbox、Hermes/OpenClaw 通过同一套入口接入。 -## 开源边界 +完整说明见 [生态定位对比](getting-started/comparison.md)。 -公开仓库包含 SDK、CLI、运行时适配、本地开发体验、人工整理文档和发布检查。 +## 核心能力 -公开仓库不发布完整 AgentEngine 控制面、内部 Kubernetes 部署自动化、内部 -kubeconfig、私有 registry、客户数据或内部支持 runbook。云端部署命令会作为 SDK -入口记录,但公开示例必须能在没有内部账号的情况下本地运行。 +| 能力 | 最常用入口 | +| --- | --- | +| Local Development | `agentengine init`、`agentengine config`、`agentengine run` | +| Browser Debugging UI | `agentengine web` | +| OpenAI-Compatible API | `/v1/responses`、`/v1/chat/completions` | +| Unified Runtime | ADK / LangGraph / LangChain / DeepAgents Runner | +| Sandbox Execution | Skill Runtime、Workspace tools、Sandbox tools | +| Serverless Deployment | `agentengine build`、`agentengine launch` | +| Hermes & OpenClaw Runtime | `agentengine hermes ...`、`agentengine openclaw ...` | -## 第一个工作流 +## 样例 -```bash -python -m venv .venv -source .venv/bin/activate -pip install -U ksadk +公开样例仓库按场景组织,而不是只按技术框架分类: -agentengine init my-agent -f langgraph -cd my-agent -agentengine config set OPENAI_API_KEY=sk-test OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_MODEL_NAME=my-model -agentengine run -i -``` +- [KSADK Samples](https://github.com/kingsoftcloud/ksadk-samples) +- 知识助手(Knowledge Assistant):知识库问答和 RAG。 +- 工作流 Agent(Workflow Agent):LangGraph + AgentEngine toolsets。 +- 工具调用 Agent(Tool-Using Agent):自定义工具调用。 +- 记忆增强 Agent(Memory-aware Agent):短期记忆和长期记忆接入。 -然后启动本地 Web UI: +## 部署 + +KsADK 支持本地优先开发,也提供经过审核后可使用的部署入口: ```bash -agentengine web . --no-open +agentengine build . +agentengine launch . --target serverless +agentengine dashboard open ``` -如果已经有一个 Agent 文件,可以导入: +Hermes 和 OpenClaw 更新已有实例时默认保留服务端已有 env、storage、network、memory 配置,只在显式传入对应 CLI 参数时覆盖,避免升级镜像时误改用户配置。 + +## 可观测 + +KsADK 原生面向 OpenTelemetry 设计。 ```bash -agentengine init my-agent --from-agent ./agent.py -cd my-agent -agentengine run . -i +OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com +OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20token ``` -## 文档地图 - -- [初识 KsADK](getting-started/concepts.md):SDK、项目配置、运行时和 Web UI 如何协作。 -- [快速开始](getting-started/quickstart.md):创建、配置、运行和调试本地 Agent。 -- [配置项](getting-started/configuration.md):环境变量和项目 YAML。 -- [项目结构](getting-started/project-structure.md):模板创建的文件以及不应提交的文件。 -- [构建 LangGraph 智能体](tutorials/langgraph-agent.md):包含完整源码的本地项目。 -- [接入已有智能体](tutorials/existing-agent.md):包装已有文件或包。 -- [本地 Web UI](guides/local-web-ui.md):本地浏览器调试和独立 `ksadk-web` 仓库计划。 -- [Web UI 仓库](guides/web-ui-source.md):`ksadk-web`、hosted UI 和 Python wheel 的关系。 -- [运行时产品](guides/runtime-products.md):Hermes 和 OpenClaw 的生命周期、runtime surface 和公开安全边界。 -- [框架接入](guides/frameworks.md):ADK、LangGraph、LangChain 和 DeepAgents 约定。 -- [Agent 最佳实践](guides/agent-best-practices.md):LangGraph/ADK 模式,以及知识库、记忆库、会话、Skill Runtime、MCP 和 workspace 文件。 -- [工具与 Skill Runtime](guides/tools-and-skill-runtime.md):内置 toolsets、focused/dispatcher 渐进式披露、Tool Gateway、MCP/A2A 和 Skill Runtime 边界。 -- [可观测与链路追踪](guides/observability-tracing.md):本地 spans、Langfuse、OTLP 和 trace metadata 规则。 -- [构建与打包](guides/build-and-package.md):本地构建、审核 gate 和公开 artifact 规则。 -- [命令行参考](reference/cli.md):公开命令面和常见选项。 -- [OpenAI 兼容 API](reference/openai-compatible-api.md):本地协议形态和 KsADK 扩展。 -- [项目配置](reference/project-config.md):YAML 和环境字段参考。 -- [远程运行时 API](reference/remote-runtime-api.md):PublicEndpoint、鉴权、OpenAI 兼容路由、workspace files、Hermes 和 OpenClaw。 -- [环境变量](reference/environment-variables.md):模型、会话、记忆、知识库、Skill Runtime、MCP、构建和 tracing 变量。 -- [会话与文件](reference/runtime-sessions-files.md):session id、上传、工作区预览和本地状态。 -- [安全边界](reference/security-boundaries.md):公开仓库、工作区、预览、包和历史记录边界。 -- [故障排查](reference/troubleshooting.md):常见安装、运行时和打包问题。 - -## 发布状态 - -计划公开位置: - -- Python SDK 仓库:`https://github.com/kingsoftcloud/ksadk-python` -- Python SDK 文档:`https://kingsoftcloud.github.io/ksadk-python/` -- Web UI 仓库:`https://github.com/kingsoftcloud/ksadk-web` -- Web UI 文档或演示:`https://kingsoftcloud.github.io/ksadk-web/` - -第一次真实源码导入必须先完成内部审核,然后才能启用 GitHub source、 -GitHub Pages、GitHub Releases 或 PyPI 发布。 +可对接: + +- Langfuse +- Arize +- Datadog +- Grafana +- Phoenix + +配置一次,到处观测。 + +## 文档 + +- [Getting Started 入门](getting-started/quickstart.md) +- [Build 构建](tutorials/langgraph-agent.md) +- [Run 运行](guides/local-web-ui.md) +- [Deploy 部署](guides/build-and-package.md) +- [Observe 观测](guides/observability-tracing.md) +- [Extend 扩展](guides/tools-and-skill-runtime.md) +- [Reference 参考](reference/cli.md) + +## 社区 + +- 仓库: +- Wiki: +- 示例仓库: +- Web UI 仓库: +- PyPI: +- 开源协议:Apache-2.0 diff --git a/public-docs/reference/security-boundaries.md b/public-docs/reference/security-boundaries.md index 1126877..93440fe 100644 --- a/public-docs/reference/security-boundaries.md +++ b/public-docs/reference/security-boundaries.md @@ -30,5 +30,5 @@ Web UI 源码属于独立 `kingsoftcloud/ksadk-web` 仓库。构建静态产物 ## 发布边界 -公开 GitHub source import、Pages、release、PyPI/TestPyPI 上传必须在内部 review +公开 GitHub source import、Pages、release、PyPI/TestPyPI 上传必须在维护者 review 和维护者审批之后执行。审批前公开仓只保留 placeholder。 diff --git a/pyproject.toml b/pyproject.toml index 3778bda..af07f01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "ksadk" -version = "0.6.3" -description = "Kingsoft Cloud Agent Development Kit - 支持 LangChain/LangGraph/DeepAgents/ADK/OpenClaw/Hermes 的本地运行与云端部署" +version = "0.6.4" +description = "KsADK Agent Runtime Platform - unified runtime, debugging, deployment and observability for AI agents" readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" diff --git a/scripts/check_approval_record.py b/scripts/check_approval_record.py new file mode 100644 index 0000000..5b26c97 --- /dev/null +++ b/scripts/check_approval_record.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Validate the public release approval record before external writes. + +The approval record is intentionally local release evidence. It should be +provided before running commands that write to GitHub Releases, TestPyPI, or +PyPI. The checker only validates public-safe decisions and avoids embedding +private repository URLs or internal review-channel names. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Sequence + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_APPROVAL_RECORD = REPO_ROOT / "docs" / "maintainer-approval-record.md" +REQUIRED_SIGNOFF_ROLES = ("Maintainer", "Security reviewer", "Release owner") +STRATEGIES = ( + "Reviewed GitHub pull request", + "Clean export from reviewed candidate", + "Rewritten Git history after secret scan", +) + + +@dataclass(frozen=True) +class ApprovalCheck: + name: str + ok: bool + detail: str + + +def _current_version() -> str: + pyproject = REPO_ROOT / "pyproject.toml" + for line in pyproject.read_text(encoding="utf-8").splitlines(): + if line.startswith("version = "): + return line.split("=", 1)[1].strip().strip('"') + raise RuntimeError("pyproject.toml 中未找到 version") + + +def _expected_decisions(version: str) -> dict[str, str]: + return { + "License": "Apache-2.0", + "Python repository": "kingsoftcloud/ksadk-python", + "Web UI repository": "kingsoftcloud/ksadk-web", + "Python package version": version, + "Public docs URL": "https://kingsoftcloud.github.io/ksadk-python/", + "Package metadata repository URL": "https://github.com/kingsoftcloud/ksadk-python", + "Package metadata documentation URL": "https://kingsoftcloud.github.io/ksadk-python/", + "Security contact": "security@kingsoft.com", + } + + +def _table_rows(text: str, section: str) -> list[list[str]]: + match = re.search(rf"^## {re.escape(section)}\n(?P.*?)(?=^## |\Z)", text, re.M | re.S) + if not match: + return [] + rows: list[list[str]] = [] + for line in match.group("body").splitlines(): + if not line.startswith("|"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if not cells or all(set(cell) <= {"-"} for cell in cells): + continue + if cells[0].lower() in {"decision", "strategy", "role"}: + continue + rows.append(cells) + return rows + + +def _clean_cell(value: str) -> str: + value = value.strip() + if len(value) >= 2 and value.startswith("`") and value.endswith("`"): + return value[1:-1].strip() + return value + + +def _decision_map(text: str) -> dict[str, str]: + return { + _clean_cell(cells[0]): _clean_cell(cells[1]) + for cells in _table_rows(text, "Required Approval Decisions") + if len(cells) >= 2 + } + + +def _strategy_map(text: str) -> dict[str, str]: + return { + cells[0]: cells[1] + for cells in _table_rows(text, "Publication Strategy") + if len(cells) >= 2 + } + + +def _signoff_rows(text: str) -> dict[str, list[str]]: + return { + cells[0]: cells + for cells in _table_rows(text, "Approval Sign-Off") + if len(cells) >= 4 + } + + +def _source_ref(text: str, name: str) -> str: + match = re.search(rf"^-\s*`{re.escape(name)}`\s*:\s*(?P.+?)\s*$", text, re.M) + return _clean_cell(match.group("value")) if match else "" + + +def _current_commit() -> str: + try: + completed = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=REPO_ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except (OSError, subprocess.CalledProcessError): + return "" + return completed.stdout.strip() + + +def _source_ref_is_filled(value: str) -> bool: + normalized = value.strip().lower() + return bool(normalized) and normalized not in { + "tbd", + "todo", + "no", + "none", + "n/a", + "", + } + + +def validate_approval_record( + path: Path, + *, + version: str, + expected_current_commit: str = "", +) -> list[ApprovalCheck]: + if not path.is_file(): + return [ApprovalCheck("approval-record:file", False, f"missing file: {path}")] + text = path.read_text(encoding="utf-8") + checks: list[ApprovalCheck] = [] + + decisions = _decision_map(text) + for decision, expected in _expected_decisions(version).items(): + actual = decisions.get(decision, "") + checks.append( + ApprovalCheck( + name=f"decision:{decision}", + ok=actual == expected, + detail=json.dumps({"actual": actual, "expected": expected}, ensure_ascii=False), + ) + ) + + strategies = _strategy_map(text) + approved = [name for name in STRATEGIES if strategies.get(name, "").lower() == "yes"] + checks.append( + ApprovalCheck( + name="publication-strategy:single-approved", + ok=len(approved) == 1, + detail=json.dumps( + {"approved": approved, "expected": "exactly one reviewed publication strategy"}, + ensure_ascii=False, + ), + ) + ) + + python_source_ref = _source_ref(text, "ksadk-python") + web_source_ref = _source_ref(text, "ksadk-web") + source_refs = { + "ksadk-python": python_source_ref, + "ksadk-web": web_source_ref, + } + for source_name, source_ref in source_refs.items(): + checks.append( + ApprovalCheck( + name=f"publication-strategy:{source_name}-source", + ok=_source_ref_is_filled(source_ref), + detail=json.dumps( + { + "actual": source_ref, + "expected": "approved source reference", + }, + ensure_ascii=False, + ), + ) + ) + if expected_current_commit: + checks.append( + ApprovalCheck( + name=f"publication-strategy:{source_name}-current-commit", + ok=expected_current_commit in source_ref, + detail=json.dumps( + { + "actual": source_ref, + "expectedCommit": expected_current_commit, + }, + ensure_ascii=False, + ), + ) + ) + + signoffs = _signoff_rows(text) + for role in REQUIRED_SIGNOFF_ROLES: + cells = signoffs.get(role, []) + filled = len(cells) >= 4 and all(cell.strip() for cell in cells[1:4]) + checks.append( + ApprovalCheck( + name=f"signoff:{role}", + ok=filled, + detail="name, decision, and date must be filled", + ) + ) + + return checks + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--approval-record", type=Path, default=DEFAULT_APPROVAL_RECORD) + parser.add_argument("--version", default=_current_version()) + parser.add_argument( + "--expected-current-commit", + default=None, + help="commit SHA that approved source references must include; defaults to git rev-parse HEAD", + ) + parser.add_argument("--json", action="store_true", help="print JSON output") + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + expected_current_commit = ( + _current_commit() if args.expected_current_commit is None else args.expected_current_commit + ) + checks = validate_approval_record( + args.approval_record, + version=args.version, + expected_current_commit=expected_current_commit, + ) + ok = all(check.ok for check in checks) + payload = { + "ok": ok, + "approvalRecord": str(args.approval_record), + "checks": [asdict(check) for check in checks], + } + if args.json: + print(json.dumps(payload, ensure_ascii=False, indent=2)) + else: + print(f"approval record: {args.approval_record}") + for check in checks: + state = "ok" if check.ok else "fail" + print(f"{state}: {check.name} - {check.detail}") + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_publication_state.py b/scripts/check_publication_state.py index 48c34ad..fe21ba6 100644 --- a/scripts/check_publication_state.py +++ b/scripts/check_publication_state.py @@ -1,15 +1,19 @@ """检查公开发布状态。 -发布前使用 `--phase pre-publish`,确保 GitHub Pages 可访问且 PyPI 上还没有 -当前版本;发布后使用 `--phase post-publish`,确保 PyPI 已能查询到当前版本。 +发布前使用 `--phase pre-publish`,确保 GitHub Pages 可访问、历史 GitHub +Release 保留,且 PyPI 上还没有当前版本;发布后使用 `--phase post-publish`, +确保 PyPI 已能查询到当前版本。主包和兼容别名包都按同一版本检查, +避免只发布其中一个包。 """ from __future__ import annotations import argparse import json +import os import sys import urllib.error +import urllib.parse import urllib.request from pathlib import Path @@ -26,7 +30,12 @@ def _current_version() -> str: def _open(url: str) -> tuple[int, bytes]: - request = urllib.request.Request(url, headers={"User-Agent": "ksadk-publication-check"}) + headers = {"User-Agent": "ksadk-publication-check"} + if urllib.parse.urlparse(url).hostname == "api.github.com": + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(request, timeout=20) as response: return response.status, response.read() @@ -65,6 +74,26 @@ def _pypi_version_exists(project: str, version: str) -> bool: return True +def _github_release_tags(url: str) -> set[str]: + status, body = _open(url) + if status != 200: + raise RuntimeError(f"github releases: 期望 HTTP 200,实际 {status}: {url}") + data = json.loads(body) + if not isinstance(data, list): + raise RuntimeError("github releases: 响应不是 release 列表") + return {str(item.get("tag_name") or "") for item in data if isinstance(item, dict)} + + +def _expect_release_history(url: str, required_tags: list[str]) -> None: + if not required_tags: + return + tags = _github_release_tags(url) + missing = [tag for tag in required_tags if tag not in tags] + if missing: + raise RuntimeError(f"GitHub Release 历史缺失: {', '.join(missing)}") + print(f"github releases: required history present: {', '.join(required_tags)}") + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--phase", choices=("pre-publish", "post-publish"), required=True) @@ -72,9 +101,20 @@ def main() -> int: parser.add_argument("--project", default="ksadk") parser.add_argument("--alias-project", default="agentengine-sdk-python") parser.add_argument("--docs-url", default="https://kingsoftcloud.github.io/ksadk-python/") + parser.add_argument( + "--github-releases-url", + default="https://api.github.com/repos/kingsoftcloud/ksadk-python/releases?per_page=100", + ) + parser.add_argument( + "--required-release-tags", + default="v0.6.1,v0.6.2,v0.6.3", + help="逗号分隔的历史 GitHub Release tag;传空字符串可跳过检查", + ) args = parser.parse_args() _expect_http_ok("docs", args.docs_url) + required_tags = [tag.strip() for tag in args.required_release_tags.split(",") if tag.strip()] + _expect_release_history(args.github_releases_url, required_tags) latest = _pypi_project_version(args.project) print(f"pypi:{args.project}: latest={latest}") @@ -85,10 +125,17 @@ def main() -> int: alias_latest = _pypi_project_version(args.alias_project) print(f"pypi:{args.alias_project}: latest={alias_latest}") + alias_exists = _pypi_version_exists(args.alias_project, args.version) + print(f"pypi:{args.alias_project}=={args.version}: exists={alias_exists}") + if args.phase == "pre-publish" and exists: raise RuntimeError(f"发布前检查失败:PyPI 已存在 {args.project}=={args.version}") + if args.phase == "pre-publish" and alias_exists: + raise RuntimeError(f"发布前检查失败:PyPI 已存在 {args.alias_project}=={args.version}") if args.phase == "post-publish" and not exists: raise RuntimeError(f"发布后检查失败:PyPI 尚未存在 {args.project}=={args.version}") + if args.phase == "post-publish" and not alias_exists: + raise RuntimeError(f"发布后检查失败:PyPI 尚未存在 {args.alias_project}=={args.version}") print(f"✅ publication {args.phase} check passed for {args.project}=={args.version}") return 0 diff --git a/scripts/generate_public_assets.py b/scripts/generate_public_assets.py new file mode 100644 index 0000000..8113c50 --- /dev/null +++ b/scripts/generate_public_assets.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +"""Generate public README and documentation visual assets.""" + +from __future__ import annotations + +import io +import os +import select +import shutil +import socket +import subprocess +import sys +import tempfile +import threading +import time +from contextlib import contextmanager +from pathlib import Path +from types import SimpleNamespace + +from PIL import Image +from rich.ansi import AnsiDecoder +from rich.console import Console +from rich.terminal_theme import TerminalTheme + + +ROOT = Path(__file__).resolve().parents[1] +ASSETS_DIR = ROOT / "public-docs" / "assets" +ARCH_SVG = ASSETS_DIR / "ksadk-runtime-architecture.svg" +ARCH_PNG = ASSETS_DIR / "ksadk-runtime-architecture.png" +HERO_PNG = ASSETS_DIR / "ksadk-runtime-platform-hero.png" +DEMO_GIF = ASSETS_DIR / "ksadk-local-debugging-demo.gif" +WEB_UI_SCREENSHOT = ASSETS_DIR / "ksadk-web-ui-screenshot.png" + +CLI_SCREENSHOT_THEME = TerminalTheme( + background=(255, 255, 255), + foreground=(34, 34, 34), + normal=[ + (34, 34, 34), + (220, 38, 38), + (0, 128, 96), + (128, 128, 0), + (0, 120, 140), + (160, 64, 160), + (0, 128, 160), + (120, 120, 120), + ], + bright=[ + (0, 0, 0), + (255, 87, 51), + (0, 150, 110), + (255, 193, 7), + (0, 140, 170), + (180, 80, 180), + (0, 150, 180), + (80, 80, 80), + ], +) + + +def generate_architecture_svg() -> None: + ASSETS_DIR.mkdir(parents=True, exist_ok=True) + svg = """ + KsADK Agent Runtime Platform 架构图 + 从 Agent 代码到 KsADK SDK、统一运行时、Skill Runtime、Workspace、Sandbox、Memory Knowledge、AgentEngine、Serverless、Hermes 和 OpenClaw 的运行链路。 + + + + + + + + + + + + + + + + + + + + + + + KsADK Agent Runtime Platform + 一次构建 Agent,到处运行:统一开发、调试、运行、沙箱、部署和观测体验 + + + AGENT CODE + + + Google ADK + + LangGraph + + LangChain + + DeepAgents + + + + + + + KsADK SDK + Runner 适配 / 配置管理 / Toolsets / 项目打包 + + + + + + + 统一运行时 + CLI / Browser Web UI / OpenAI-Compatible API / Streaming Sessions + 本地开发时即验证部署后的运行边界 + + + + + + + + + + + Skill Runtime + Skill Space / workflow + + + Workspace + 会话文件 / artifacts + + + Sandbox + 隔离命令 / 代码执行 + + + Memory & Knowledge + 长期记忆 / 知识库 + + + + + + + + + + AgentEngine + 远端运行、服务入口与平台能力 + + + + + + + Serverless / Hermes / OpenClaw Runtime + + +""" + ARCH_SVG.write_text(svg, encoding="utf-8") + + +def render_architecture_png() -> None: + if ARCH_PNG.exists() and os.environ.get("KSADK_REGENERATE_ARCHITECTURE_PNG") != "1": + return + converter = shutil.which("rsvg-convert") + if converter is None: + raise RuntimeError("rsvg-convert is required to render architecture PNG") + subprocess.run( + [converter, str(ARCH_SVG), "--width", "1600", "--output", str(ARCH_PNG)], + check=True, + ) + + +def _capture_cli_help_plain() -> str: + env = os.environ.copy() + env.pop("NO_COLOR", None) + env.pop("AGENTENGINE_NO_COLOR", None) + env["AGENTENGINE_OUTPUT_MODE"] = "pretty" + env["COLUMNS"] = "120" + completed = subprocess.run( + [sys.executable, "-m", "ksadk.cli", "-h"], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ) + return completed.stdout + + +def _capture_cli_help_ansi() -> str: + env = os.environ.copy() + env.pop("NO_COLOR", None) + env.pop("AGENTENGINE_NO_COLOR", None) + env["AGENTENGINE_OUTPUT_MODE"] = "pretty" + env["TERM"] = "xterm-256color" + env["COLUMNS"] = "120" + + try: + import pty + + master_fd, slave_fd = pty.openpty() + except (ImportError, OSError): + return _capture_cli_help_plain() + + try: + process = subprocess.Popen( + [sys.executable, "-m", "ksadk.cli", "-h"], + cwd=ROOT, + env=env, + stdin=subprocess.DEVNULL, + stdout=slave_fd, + stderr=slave_fd, + close_fds=True, + ) + finally: + os.close(slave_fd) + + chunks: list[bytes] = [] + try: + while True: + readable, _, _ = select.select([master_fd], [], [], 0.1) + if master_fd in readable: + try: + data = os.read(master_fd, 4096) + except OSError: + break + if not data: + break + chunks.append(data) + + if process.poll() is not None: + while True: + try: + data = os.read(master_fd, 4096) + except OSError: + break + if not data: + break + chunks.append(data) + break + finally: + os.close(master_fd) + + return_code = process.wait() + if return_code != 0: + raise subprocess.CalledProcessError(return_code, [sys.executable, "-m", "ksadk.cli", "-h"]) + return b"".join(chunks).decode("utf-8", "replace") + + +def _strip_ansi(text: str) -> str: + import re + + return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text) + + +def _trim_cli_help_for_readme(output: str) -> str: + lines = output.replace("\r\n", "\n").splitlines() + selected: list[str] = [] + for line in lines: + plain = _strip_ansi(line) + if "可用命令" in plain or "Available Commands" in plain: + break + selected.append(line) + return "\n".join(selected).rstrip() + "\n" + + +def generate_hero_png() -> None: + ASSETS_DIR.mkdir(parents=True, exist_ok=True) + converter = shutil.which("rsvg-convert") + if converter is None: + raise RuntimeError("rsvg-convert is required to render CLI screenshot PNG") + + output = _trim_cli_help_for_readme(_capture_cli_help_ansi()) + ansi = "\x1b[1;30m$ agentengine -h\x1b[0m\n" + output + console = Console( + record=True, + width=118, + force_terminal=True, + color_system="truecolor", + file=io.StringIO(), + highlight=False, + ) + decoder = AnsiDecoder() + for line in decoder.decode(ansi): + console.print(line, markup=False, highlight=False) + + svg = console.export_svg(title="agentengine -h", theme=CLI_SCREENSHOT_THEME) + temp_path: Path | None = None + try: + with tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + suffix=".svg", + prefix="ksadk-cli-help-", + dir=ASSETS_DIR, + delete=False, + ) as temp_file: + temp_file.write(svg) + temp_path = Path(temp_file.name) + subprocess.run( + [converter, str(temp_path), "--width", "1600", "--output", str(HERO_PNG)], + check=True, + ) + finally: + if temp_path is not None: + temp_path.unlink(missing_ok=True) + + +def _find_chromium_executable() -> str | None: + explicit_path = os.environ.get("KSADK_ASSET_CHROMIUM") + if explicit_path and Path(explicit_path).is_file(): + return explicit_path + + candidates: list[Path] = [] + cache_roots = [ + Path.home() / "Library" / "Caches" / "ms-playwright", + Path.home() / ".cache" / "ms-playwright", + ] + for cache_root in cache_roots: + candidates.extend( + cache_root.glob( + "chromium-*/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" + ) + ) + candidates.extend( + cache_root.glob( + "chromium-*/chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" + ) + ) + candidates.extend(cache_root.glob("chromium-*/chrome-linux/chrome")) + + for candidate in candidates: + if candidate.is_file(): + return str(candidate) + + for executable_name in ( + "chromium", + "chromium-browser", + "google-chrome", + "google-chrome-stable", + "chrome", + ): + resolved = shutil.which(executable_name) + if resolved: + return resolved + return None + + +class _PublicDemoRunner: + """用于公开资产生成的 deterministic runner,不连接外部模型或云环境。""" + + def __init__(self): + from ksadk.runners.base_runner import BaseRunner + + class Runner(BaseRunner): + def __init__(self): + super().__init__( + detection_result=SimpleNamespace( + name="runtime-platform-demo", + description="KsADK 真实 Web UI 演示", + type=SimpleNamespace(value="langgraph"), + ), + project_dir=str(ROOT), + ) + + def load_agent(self) -> None: + return None + + async def invoke(self, input_data: dict) -> dict: + return { + "output": ( + "KsADK 已完成本地调试检查:运行时、Workspace、Sandbox " + "与工具调用链路均可在 Web UI 中观察。" + ) + } + + async def stream(self, input_data: dict): + import asyncio + + yield { + "type": "tool_call", + "tool_name": "workspace_status", + "tool_args": {"path": "/workspace", "include_artifacts": True}, + "status": "running", + } + await asyncio.sleep(0.45) + yield { + "type": "tool_result", + "tool_name": "workspace_status", + "tool_output": '{"workspace":"ready","artifacts":3,"sandbox":"enabled"}', + } + await asyncio.sleep(0.45) + yield { + "type": "thinking", + "delta": "正在检查 Skill Runtime、Workspace、Sandbox 和长期记忆配置边界。", + } + await asyncio.sleep(0.45) + for delta in ( + "KsADK 已接入 LangGraph Runner,", + "本地 Web UI 正在通过 Responses 流式协议返回结果。", + "\n\n- Workspace:可浏览会话文件和 artifacts", + "\n- Sandbox:支持隔离命令执行", + "\n- Skills:未配置 Skill Space 时会明确降级,不伪造工具结果", + ): + yield {"type": "text", "delta": delta} + await asyncio.sleep(0.35) + yield { + "type": "responses_output", + "response_id": "resp_public_demo", + "output": [ + { + "id": "call_workspace_status", + "type": "function_call", + "name": "workspace_status", + "arguments": '{"path":"/workspace"}', + } + ], + } + yield { + "type": "final", + "output": ( + "KsADK 已完成本地调试检查:运行时、Workspace、Sandbox " + "与工具调用链路均可在 Web UI 中观察。" + ), + } + + self.runner = Runner() + + +@contextmanager +def _temporary_env(values: dict[str, str | None]): + original = {key: os.environ.get(key) for key in values} + try: + for key, value in values.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + yield + finally: + for key, value in original.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + +@contextmanager +def _run_public_demo_server(): + import importlib + + import uvicorn + from ksadk.sessions.in_memory import InMemorySessionService + + server_app_module = importlib.import_module("ksadk.server.app") + service = InMemorySessionService() + demo_runner = _PublicDemoRunner().runner + server_app_module.resolve_session_service = lambda: service + server_app_module.set_runner(demo_runner) + + sock = socket.socket() + sock.bind(("127.0.0.1", 0)) + host, port = sock.getsockname() + sock.close() + + config = uvicorn.Config(server_app_module.app, host=host, port=port, log_level="warning") + server = uvicorn.Server(config) + thread = threading.Thread(target=server.run, daemon=True) + thread.start() + + deadline = time.time() + 8 + while not server.started and time.time() < deadline: + time.sleep(0.05) + + if not server.started: + server.should_exit = True + thread.join(timeout=5) + raise RuntimeError("KsADK Web UI demo server failed to start") + + try: + yield f"http://{host}:{port}" + finally: + server.should_exit = True + thread.join(timeout=5) + + +def _save_web_ui_gif(frame_paths: list[Path]) -> None: + frames: list[Image.Image] = [] + for frame_path in frame_paths: + image = Image.open(frame_path).convert("RGB") + target_width = 1100 + target_height = round(image.height * target_width / image.width) + image = image.resize((target_width, target_height), Image.Resampling.LANCZOS) + frames.append(image) + + durations = [900, 900, 1000, 1200, 1800] + frames[0].save( + DEMO_GIF, + save_all=True, + append_images=frames[1:], + duration=durations[: len(frames)], + loop=0, + optimize=True, + ) + + +def generate_web_ui_assets() -> None: + try: + from playwright.sync_api import sync_playwright + except ImportError as exc: + raise RuntimeError( + "playwright is required to generate real Web UI assets. " + "Install dev dependencies and run `python -m playwright install chromium`." + ) from exc + + chromium = _find_chromium_executable() + if chromium is None: + raise RuntimeError( + "Chromium is required to generate real Web UI assets. " + "Set KSADK_ASSET_CHROMIUM or run `python -m playwright install chromium`." + ) + + ASSETS_DIR.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="ksadk-public-web-ui-") as tmp_dir: + tmp_path = Path(tmp_dir) + frame_paths = [tmp_path / f"frame-{index}.png" for index in range(5)] + with _temporary_env( + { + "AGENTENGINE_UI_DIR": str(tmp_path / ".agentengine" / "ui"), + "OPENAI_MODEL_NAME": "gpt-4o-mini", + "OPENAI_API_KEY": None, + "OPENAI_BASE_URL": None, + "OPENAI_API_BASE": None, + } + ): + with _run_public_demo_server() as base_url: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(executable_path=chromium, headless=True) + try: + page = browser.new_page( + viewport={"width": 1440, "height": 940}, + device_scale_factor=1, + ) + page.goto(f"{base_url}/chat") + page.wait_for_load_state("networkidle") + page.wait_for_selector("textarea") + page.screenshot(path=str(frame_paths[0]), full_page=False) + page.locator("textarea").fill( + "请检查这个 Agent 的工具、Workspace、Sandbox 和长期记忆配置" + ) + page.screenshot(path=str(frame_paths[1]), full_page=False) + page.locator('button[type="submit"]').click() + page.wait_for_timeout(900) + page.screenshot(path=str(frame_paths[2]), full_page=False) + page.wait_for_timeout(1200) + page.screenshot(path=str(frame_paths[3]), full_page=False) + page.wait_for_function( + "() => document.body.innerText.includes('运行完成')", + timeout=10000, + ) + page.screenshot(path=str(frame_paths[4]), full_page=False) + finally: + browser.close() + + final_image = Image.open(frame_paths[-1]).convert("RGB") + final_image.save(WEB_UI_SCREENSHOT, optimize=True) + _save_web_ui_gif(frame_paths) + + +def main() -> int: + generate_hero_png() + generate_architecture_svg() + render_architecture_png() + generate_web_ui_assets() + print(f"generated {HERO_PNG.relative_to(ROOT)}") + print(f"generated {ARCH_SVG.relative_to(ROOT)}") + print(f"generated {ARCH_PNG.relative_to(ROOT)}") + print(f"generated {WEB_UI_SCREENSHOT.relative_to(ROOT)}") + print(f"generated {DEMO_GIF.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/open_source_audit.py b/scripts/open_source_audit.py index d8a8375..ecb5008 100644 --- a/scripts/open_source_audit.py +++ b/scripts/open_source_audit.py @@ -97,6 +97,12 @@ def to_dict(self) -> dict[str, object]: ) PUBLIC_REPO_RULES = COMMON_RULES + ( + DenyRule( + name="non-curated-docs", + prefixes=("docs/",), + allowed_paths=("docs/maintainer-approval-record.md",), + description="internal planning and technical design docs stay out of the public repository; user docs live in public-docs/", + ), DenyRule( name="internal-deploy-material", prefixes=( @@ -124,11 +130,6 @@ def to_dict(self) -> dict[str, object]: contains=("/.env.example",), description="environment examples must be curated before publication to avoid private endpoints or credential names", ), - DenyRule( - name="non-curated-docs", - prefixes=("docs/",), - description="internal planning and technical design docs stay out of the public repository; user docs live in public-docs/", - ), ) WHEEL_RULES = ( @@ -210,7 +211,7 @@ def to_dict(self) -> dict[str, object]: name="internal-service-endpoint", pattern=re.compile( r"(? dict[str, object]: ".md", ".py", ".sh", + ".svg", ".toml", ".ts", ".tsx", diff --git a/scripts/prepare_ksadk_python_export.py b/scripts/prepare_ksadk_python_export.py index 3e1f9aa..79cb0a0 100644 --- a/scripts/prepare_ksadk_python_export.py +++ b/scripts/prepare_ksadk_python_export.py @@ -27,7 +27,7 @@ DEFAULT_OUTPUT_DIR = Path("/tmp/ksadk-python-export-candidate") -CURATED_DOCS: set[str] = set() +CURATED_DOCS: set[str] = {"docs/maintainer-approval-record.md"} ROOT_EXPORT_FILES = { ".dockerignore", @@ -66,6 +66,9 @@ SCRIPT_EXPORT_FILES = { "scripts/audit_release_artifacts.py", + "scripts/check_approval_record.py", + "scripts/check_publication_state.py", + "scripts/generate_public_assets.py", "scripts/open_source_audit.py", "scripts/prepare_ksadk_python_export.py", "scripts/prepare_ksadk_web_export.py", @@ -73,7 +76,12 @@ PUBLIC_TEST_FILES = { "tests/conftest.py", + "tests/test_check_approval_record.py", + "tests/test_check_publication_state.py", + "tests/test_markdown_repair.py", "tests/test_open_source_audit.py", + "tests/test_public_positioning_docs.py", + "tests/test_public_release_gates.py", "tests/test_prepare_ksadk_python_export.py", "tests/test_prepare_ksadk_web_export.py", "tests/test_runtime_common_packaging.py", diff --git a/tests/skills/test_service_client_http.py b/tests/skills/test_service_client_http.py index 91273af..dadaae3 100644 --- a/tests/skills/test_service_client_http.py +++ b/tests/skills/test_service_client_http.py @@ -243,7 +243,7 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(404) client = SkillServiceClient( - base_url="http://maicp.inner.api.ksyun.com", + base_url="http://aicp.inner.api.ksyun.com", account_id="2000003485", transport=httpx.MockTransport(handler), ) @@ -255,7 +255,7 @@ def handler(request: httpx.Request) -> httpx.Response: method, url, headers = requests[0] assert method == "GET" assert url == ( - "http://maicp.inner.api.ksyun.com/" + "http://aicp.inner.api.ksyun.com/" "?Action=ListSkillsBySpaceId&Version=2024-06-12" "&SpaceId=ss-1&PageNumber=1&PageSize=100" ) @@ -318,7 +318,7 @@ def handler(request: httpx.Request) -> httpx.Response: client = SkillServiceClient( base_url="http://aicp.inner.api.ksyun.com", - account_id="73398439", + account_id="2000003485", transport=httpx.MockTransport(handler), ) @@ -334,7 +334,7 @@ def handler(request: httpx.Request) -> httpx.Response: ) assert headers["x-ksc-region"] == "cn-beijing-6" assert headers["x-ksc-custom-source"] == "pre" - assert headers["x-ksc-account-id"] == "73398439" + assert headers["x-ksc-account-id"] == "2000003485" def test_service_client_uses_registered_kop_action_for_available_premade_skills(): @@ -364,7 +364,7 @@ def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(404) client = SkillServiceClient( - base_url="http://maicp.inner.api.ksyun.com", + base_url="http://aicp.inner.api.ksyun.com", account_id="2000003485", transport=httpx.MockTransport(handler), ) @@ -376,7 +376,7 @@ def handler(request: httpx.Request) -> httpx.Response: method, url, headers = requests[0] assert method == "GET" assert url == ( - "http://maicp.inner.api.ksyun.com/" + "http://aicp.inner.api.ksyun.com/" "?Action=ListAvailablePremadeSkills&Version=2024-06-12" ) assert headers["x-action"] == "ListAvailablePremadeSkills" diff --git a/tests/test_check_approval_record.py b/tests/test_check_approval_record.py new file mode 100644 index 0000000..493895f --- /dev/null +++ b/tests/test_check_approval_record.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT_PATH = REPO_ROOT / "scripts" / "check_approval_record.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("check_approval_record", SCRIPT_PATH) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _approved_record(python_source: str = "cd5fa22b1e78f03a8a9d025017e97ad414fdaa74") -> str: + return """# ksadk Public Release Approval Record + +## Required Approval Decisions + +| Decision | Approved value | +| --- | --- | +| License | Apache-2.0 | +| Python repository | kingsoftcloud/ksadk-python | +| Web UI repository | kingsoftcloud/ksadk-web | +| Python package version | 0.6.4 | +| Public docs URL | https://kingsoftcloud.github.io/ksadk-python/ | +| Package metadata repository URL | https://github.com/kingsoftcloud/ksadk-python | +| Package metadata documentation URL | https://kingsoftcloud.github.io/ksadk-python/ | +| Security contact | security@kingsoft.com | + +## Publication Strategy + +| Strategy | Approved | +| --- | --- | +| Reviewed GitHub pull request | No | +| Clean export from reviewed candidate | Yes | +| Rewritten Git history after secret scan | No | + +The approved strategy must name the commit, tag, or export archive used for: + +- `ksadk-python`: {python_source} +- `ksadk-web`: /tmp/ksadk-web-export-candidate + +## Approval Sign-Off + +| Role | Name | Decision | Date | +| --- | --- | --- | --- | +| Maintainer | Alice | Approved | 2026-05-28 | +| Security reviewer | Bob | Approved | 2026-05-28 | +| Release owner | Carol | Approved | 2026-05-28 | +""".format(python_source=python_source) + + +def test_template_approval_record_fails_until_strategy_and_signoffs_are_filled(): + module = _load_module() + + checks = module.validate_approval_record( + REPO_ROOT / "docs" / "maintainer-approval-record.md", + version="0.6.4", + expected_current_commit="current-reviewed-commit", + ) + + failed = {check.name for check in checks if not check.ok} + assert "publication-strategy:single-approved" in failed + assert "publication-strategy:ksadk-python-source" in failed + assert "publication-strategy:ksadk-web-source" in failed + assert "publication-strategy:ksadk-python-current-commit" in failed + assert "publication-strategy:ksadk-web-current-commit" in failed + assert "signoff:Maintainer" in failed + assert "signoff:Security reviewer" in failed + assert "signoff:Release owner" in failed + assert all(check.ok for check in checks if check.name.startswith("decision:")) + + +def test_filled_approval_record_passes(tmp_path): + module = _load_module() + record = tmp_path / "approval.md" + record.write_text(_approved_record(), encoding="utf-8") + + checks = module.validate_approval_record(record, version="0.6.4", expected_current_commit="") + + assert all(check.ok for check in checks) + + +def test_filled_record_fails_when_source_references_do_not_match_current_commit(tmp_path): + module = _load_module() + record = tmp_path / "approval.md" + record.write_text(_approved_record("old-reviewed-source"), encoding="utf-8") + + checks = module.validate_approval_record( + record, + version="0.6.4", + expected_current_commit="new-reviewed-commit", + ) + + failed = {check.name for check in checks if not check.ok} + assert "publication-strategy:ksadk-python-current-commit" in failed + assert "publication-strategy:ksadk-web-current-commit" in failed + + +def test_filled_record_passes_when_source_references_include_current_commit(tmp_path): + module = _load_module() + record = tmp_path / "approval.md" + record.write_text( + _approved_record("reviewed export from new-reviewed-commit") + .replace( + "- `ksadk-web`: /tmp/ksadk-web-export-candidate", + "- `ksadk-web`: /tmp/ksadk-web-export-candidate at new-reviewed-commit", + ), + encoding="utf-8", + ) + + checks = module.validate_approval_record( + record, + version="0.6.4", + expected_current_commit="new-reviewed-commit", + ) + + assert all(check.ok for check in checks) + + +def test_cli_json_reports_failed_template_record(): + result = subprocess.run( + [sys.executable, str(SCRIPT_PATH), "--json"], + cwd=REPO_ROOT, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert result.returncode == 1 + payload = json.loads(result.stdout) + assert payload["ok"] is False + assert any( + check["name"] == "publication-strategy:single-approved" and not check["ok"] + for check in payload["checks"] + ) diff --git a/tests/test_check_publication_state.py b/tests/test_check_publication_state.py new file mode 100644 index 0000000..36cf97d --- /dev/null +++ b/tests/test_check_publication_state.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "check_publication_state.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("check_publication_state", SCRIPT_PATH) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _run_main(monkeypatch, module, *, phase: str, version_exists: dict[tuple[str, str], bool]): + monkeypatch.setattr( + sys, + "argv", + [ + "check_publication_state.py", + "--phase", + phase, + "--version", + "0.6.4", + ], + ) + monkeypatch.setattr(module, "_expect_http_ok", lambda name, url: None) + monkeypatch.setattr( + module, + "_github_release_tags", + lambda url: {"v0.6.1", "v0.6.2", "v0.6.3"}, + ) + monkeypatch.setattr( + module, + "_pypi_project_version", + lambda project: {"ksadk": "0.6.3", "agentengine-sdk-python": "0.6.2"}[project], + ) + monkeypatch.setattr( + module, + "_pypi_version_exists", + lambda project, version: version_exists.get((project, version), False), + ) + + return module.main() + + +def test_pre_publish_fails_when_alias_package_version_already_exists(monkeypatch): + module = _load_module() + + with pytest.raises(RuntimeError, match="agentengine-sdk-python==0.6.4"): + _run_main( + monkeypatch, + module, + phase="pre-publish", + version_exists={ + ("ksadk", "0.6.4"): False, + ("agentengine-sdk-python", "0.6.4"): True, + }, + ) + + +def test_post_publish_fails_when_alias_package_version_is_missing(monkeypatch): + module = _load_module() + + with pytest.raises(RuntimeError, match="agentengine-sdk-python==0.6.4"): + _run_main( + monkeypatch, + module, + phase="post-publish", + version_exists={ + ("ksadk", "0.6.4"): True, + ("agentengine-sdk-python", "0.6.4"): False, + }, + ) + + +def test_publication_state_fails_when_historical_github_release_is_missing(monkeypatch): + module = _load_module() + + monkeypatch.setattr( + sys, + "argv", + [ + "check_publication_state.py", + "--phase", + "pre-publish", + "--version", + "0.6.4", + ], + ) + monkeypatch.setattr(module, "_expect_http_ok", lambda name, url: None) + monkeypatch.setattr(module, "_github_release_tags", lambda url: {"v0.6.1", "v0.6.3"}) + + with pytest.raises(RuntimeError, match="v0.6.2"): + module.main() + + +def test_github_api_request_uses_available_token(monkeypatch): + module = _load_module() + captured = {} + + class FakeResponse: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return None + + def read(self): + return b"[]" + + def fake_urlopen(request, timeout): + captured["headers"] = dict(request.header_items()) + captured["timeout"] = timeout + return FakeResponse() + + monkeypatch.setenv("GH_TOKEN", "gh-test-token") + monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen) + + status, body = module._open("https://api.github.com/repos/kingsoftcloud/ksadk-python/releases") + + assert status == 200 + assert body == b"[]" + assert captured["headers"]["Authorization"] == "Bearer gh-test-token" + assert captured["timeout"] == 20 + + +def test_github_token_is_not_sent_to_url_containing_github_api_as_query(monkeypatch): + module = _load_module() + captured = {} + + class FakeResponse: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return None + + def read(self): + return b"ok" + + def fake_urlopen(request, timeout): + captured["headers"] = dict(request.header_items()) + return FakeResponse() + + monkeypatch.setenv("GH_TOKEN", "gh-test-token") + monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen) + + status, body = module._open("https://example.com/status?next=api.github.com") + + assert status == 200 + assert body == b"ok" + assert "Authorization" not in captured["headers"] diff --git a/tests/test_conversation_runtime.py b/tests/test_conversation_runtime.py index 1cc32f7..85321a9 100644 --- a/tests/test_conversation_runtime.py +++ b/tests/test_conversation_runtime.py @@ -28,7 +28,14 @@ stream_conversation_turn, stream_responses_conversation_turn, ) -from ksadk.runtime_context import get_current_invocation_context +from ksadk.runtime_context import ( + PlatformInvocationContext, + get_current_invocation_context_or_default, + get_current_account_id, + get_current_invocation_context, + get_current_user_id, + platform_invocation_scope, +) from ksadk.sessions.base import SessionEvent from ksadk.sessions.in_memory import InMemorySessionService from ksadk.tracing.exporters.inmemory_exporter import InMemoryExporter @@ -201,6 +208,43 @@ def _extract_sse_payload(chunks: list[str], event_name: str) -> dict: raise AssertionError(f"SSE event {event_name!r} not found") +def test_runtime_context_helpers_return_defaults_outside_invocation_scope(): + assert get_current_invocation_context() is None + context = get_current_invocation_context_or_default() + assert context.user_id == "" + assert context.account_id == "" + assert context.session_id == "" + assert context.history == [] + assert context.attachments == [] + assert get_current_user_id() == "" + assert get_current_account_id() == "" + assert get_current_user_id(default="anonymous") == "anonymous" + assert get_current_account_id(default="tenantless") == "tenantless" + + +def test_runtime_context_helpers_read_current_invocation_scope(): + context = PlatformInvocationContext( + agent_id="demo-agent", + user_id="user-1", + account_id="acct-1", + session_id="sess-1", + history=[], + input_content=[], + input_messages=[], + input_parts=[], + attachments=[], + attachment_results=[], + current_attachments=[], + current_attachment_results=[], + has_current_files=False, + runner_type="mock", + ) + + with platform_invocation_scope(context): + assert get_current_user_id() == "user-1" + assert get_current_account_id() == "acct-1" + + @pytest.fixture def in_memory_trace_exporter(): provider = TracerProvider() @@ -1488,6 +1532,7 @@ async def test_invoke_conversation_once_binds_platform_invocation_context_and_am session_id=None, messages=[{"role": "user", "content": "继续"}], model="gpt-4o", + account_id="acct-1", prepare_runner=lambda current_runner, model: current_runner.prepare_for_request(model), session_service_provider=lambda: service, ) @@ -1498,10 +1543,12 @@ async def test_invoke_conversation_once_binds_platform_invocation_context_and_am assert runner.calls[-1]["memory_context"] == {"formatted_text": "Memory facts"} assert runner.calls[-1]["platform_context"]["agent_id"] == "demo-agent" assert runner.calls[-1]["platform_context"]["user_id"] == "user-1" + assert runner.calls[-1]["platform_context"]["account_id"] == "acct-1" assert runner.calls[-1]["platform_context"]["session_id"] == session_id assert runner.captured_runtime_context is not None assert runner.captured_runtime_context.agent_id == "demo-agent" assert runner.captured_runtime_context.user_id == "user-1" + assert runner.captured_runtime_context.account_id == "acct-1" assert runner.captured_runtime_context.session_id == session_id assert runner.captured_runtime_context.kb_context == {"formatted_text": "KB facts"} assert runner.captured_runtime_context.memory_context == {"formatted_text": "Memory facts"} diff --git a/tests/test_markdown_repair.py b/tests/test_markdown_repair.py new file mode 100644 index 0000000..1fcd007 --- /dev/null +++ b/tests/test_markdown_repair.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from ksadk.markdown import repair_markdown + + +def test_repair_markdown_closes_unclosed_fenced_code_block(): + raw = "下面是示例:\n```python\nprint('hello')" + + repaired = repair_markdown(raw, enabled=True) + + assert repaired == "下面是示例:\n\n```python\nprint('hello')\n```\n" + + +def test_repair_markdown_closes_long_fenced_code_block_with_matching_marker(): + raw = "下面是示例:\n````python\nprint('``` still code')" + + repaired = repair_markdown(raw, enabled=True) + + assert repaired == "下面是示例:\n\n````python\nprint('``` still code')\n````\n" + + +def test_repair_markdown_preserves_already_closed_fenced_code_block(): + raw = "说明\n\n```python\nprint('hello')\n```\n" + + repaired = repair_markdown(raw, enabled=True) + + assert repaired == raw + + +def test_repair_markdown_normalizes_blank_lines_around_tables_and_lists(): + raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n- 第二项\n下一段" + + repaired = repair_markdown(raw, enabled=True) + + assert repaired == ( + "结果如下:\n\n" + "| 名称 | 值 |\n" + "| --- | --- |\n" + "| A | 1 |\n\n" + "结论:\n\n" + "- 第一项\n" + "- 第二项\n\n" + "下一段\n" + ) + + +def test_repair_markdown_is_disabled_by_default(): + raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n下一段" + + repaired = repair_markdown(raw) + + assert repaired == raw + + +def test_repair_markdown_can_be_enabled_with_one_switch(): + raw = "结果如下:\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n结论:\n- 第一项\n下一段" + + repaired = repair_markdown(raw, enabled=True) + + assert repaired == ( + "结果如下:\n\n" + "| 名称 | 值 |\n" + "| --- | --- |\n" + "| A | 1 |\n\n" + "结论:\n\n" + "- 第一项\n\n" + "下一段\n" + ) + + +def test_repair_markdown_is_idempotent(): + raw = "标题\n```json\n{\"ok\": true}" + + once = repair_markdown(raw, enabled=True) + twice = repair_markdown(once, enabled=True) + + assert once == twice + + +def test_repair_markdown_handles_empty_and_non_string_values(): + assert repair_markdown("") == "" + assert repair_markdown(None) == "" + assert repair_markdown(123) == "123" + assert repair_markdown(123, enabled=True) == "123\n" diff --git a/tests/test_open_source_audit.py b/tests/test_open_source_audit.py index 3d064c4..86f9e0e 100644 --- a/tests/test_open_source_audit.py +++ b/tests/test_open_source_audit.py @@ -310,9 +310,9 @@ def test_content_audit_allows_aicp_internal_endpoints_but_blocks_other_internal_ (tmp_path / "aicp.py").write_text( "\n".join( [ + 'AICP_PUBLIC = "aicp.api.ksyun.com"', 'AICP_INTERNAL = "aicp.internal.api.ksyun.com"', 'AICP_INNER = "aicp.inner.api.ksyun.com"', - 'MAICP_INNER = "maicp.inner.api.ksyun.com"', ] ), encoding="utf-8", diff --git a/tests/test_prepare_ksadk_python_export.py b/tests/test_prepare_ksadk_python_export.py index 9ae5838..25a3f22 100644 --- a/tests/test_prepare_ksadk_python_export.py +++ b/tests/test_prepare_ksadk_python_export.py @@ -43,12 +43,21 @@ def _make_git_repo(root: Path) -> None: "ksadk_runtime_common/__init__.py": "\n", "ksadk_runtime_common/schemas/event.json": "{}\n", "tests/test_open_source_audit.py": "def test_public():\n assert True\n", + "tests/test_public_positioning_docs.py": "def test_public_docs():\n assert True\n", + "tests/test_check_publication_state.py": "def test_publication_state():\n assert True\n", "tests/test_tracing_setup_otlp.py": "def test_tracing_public():\n assert True\n", "tests/test_deploy_integration.py": "def test_internal():\n assert True\n", "tests/snapshots/help_snapshots.txt": "internal snapshot\n", "public-docs/index.md": "# Public docs\n", + "public-docs/assets/ksadk-runtime-platform-hero.png": "hero\n", + "public-docs/assets/ksadk-web-ui-screenshot.png": "screenshot\n", + "public-docs/assets/ksadk-runtime-architecture.svg": "\n", + "public-docs/assets/ksadk-runtime-architecture.png": "png\n", + "public-docs/assets/ksadk-local-debugging-demo.gif": "gif\n", "scripts/open_source_audit.py": "print('audit')\n", "scripts/audit_release_artifacts.py": "print('dist audit')\n", + "scripts/check_publication_state.py": "print('publication')\n", + "scripts/generate_public_assets.py": "print('assets')\n", "scripts/prepare_ksadk_python_export.py": "print('export')\n", "scripts/prepare_ksadk_web_export.py": "print('web export')\n", "docs/internal/release-secret.md": "internal\n", @@ -115,12 +124,21 @@ def test_export_plan_selects_public_candidate_files_and_excludes_local_artifacts assert "ksadk_runtime_common/__init__.py" in plan.export_paths assert "ksadk_runtime_common/schemas/event.json" in plan.export_paths assert "tests/test_open_source_audit.py" in plan.export_paths + assert "tests/test_public_positioning_docs.py" in plan.export_paths + assert "tests/test_check_publication_state.py" in plan.export_paths assert "tests/test_tracing_setup_otlp.py" in plan.export_paths assert "public-docs/index.md" in plan.export_paths + assert "public-docs/assets/ksadk-runtime-platform-hero.png" in plan.export_paths + assert "public-docs/assets/ksadk-web-ui-screenshot.png" in plan.export_paths + assert "public-docs/assets/ksadk-runtime-architecture.svg" in plan.export_paths + assert "public-docs/assets/ksadk-runtime-architecture.png" in plan.export_paths + assert "public-docs/assets/ksadk-local-debugging-demo.gif" in plan.export_paths assert "docs/release-checklist.md" not in plan.export_paths assert "docs/ksadk开源准备计划.md" not in plan.export_paths assert "scripts/open_source_audit.py" in plan.export_paths assert "scripts/audit_release_artifacts.py" in plan.export_paths + assert "scripts/check_publication_state.py" in plan.export_paths + assert "scripts/generate_public_assets.py" in plan.export_paths assert "scripts/prepare_ksadk_python_export.py" in plan.export_paths assert "scripts/prepare_ksadk_web_export.py" in plan.export_paths assert "tests/test_deploy_integration.py" not in plan.export_paths @@ -186,6 +204,11 @@ def test_cli_writes_clean_export_candidate_and_manifest(tmp_path): assert not (output_dir / "ksadk" / "server" / "web-ui").exists() assert (output_dir / "ksadk_runtime_common" / "__init__.py").is_file() assert (output_dir / "public-docs" / "index.md").is_file() + assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-platform-hero.png").is_file() + assert (output_dir / "public-docs" / "assets" / "ksadk-web-ui-screenshot.png").is_file() + assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-architecture.svg").is_file() + assert (output_dir / "public-docs" / "assets" / "ksadk-runtime-architecture.png").is_file() + assert (output_dir / "public-docs" / "assets" / "ksadk-local-debugging-demo.gif").is_file() assert (output_dir / "export-manifest.json").is_file() assert not (output_dir / ".pypirc").exists() assert not (output_dir / "docs" / "internal").exists() diff --git a/tests/test_prepare_ksadk_web_export.py b/tests/test_prepare_ksadk_web_export.py index c1bac6e..0c0eea0 100644 --- a/tests/test_prepare_ksadk_web_export.py +++ b/tests/test_prepare_ksadk_web_export.py @@ -122,7 +122,7 @@ def test_default_hosted_root_points_to_workspace_sibling_repo(): exporter = _load_export_module() assert exporter.DEFAULT_HOSTED_ROOT.name == "agentengine-hosted-ui" - assert exporter.DEFAULT_HOSTED_ROOT.parent.name in {"agentengine", "agent-sdk"} + assert exporter.DEFAULT_HOSTED_ROOT.is_absolute() def test_resolve_default_hosted_root_supports_nested_workspace(tmp_path): diff --git a/tests/test_public_positioning_docs.py b/tests/test_public_positioning_docs.py new file mode 100644 index 0000000..7f15414 --- /dev/null +++ b/tests/test_public_positioning_docs.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +from pathlib import Path +import re +import tomllib +import yaml + + +ROOT = Path(__file__).resolve().parents[1] + + +class MkdocsTestLoader(yaml.SafeLoader): + pass + + +def _ignore_python_name(loader: MkdocsTestLoader, node: yaml.Node): + return loader.construct_scalar(node) + + +MkdocsTestLoader.add_constructor( + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", + _ignore_python_name, +) + + +def _read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def _public_markdown_and_config_files() -> list[Path]: + files = [ + ROOT / "README.md", + ROOT / "README.zh-CN.md", + ROOT / "README.en.md", + ROOT / "CHANGELOG.md", + ROOT / "mkdocs.yml", + ROOT / "pyproject.toml", + ] + files.extend(sorted((ROOT / "public-docs").rglob("*.md"))) + return files + + +PUBLIC_FORBIDDEN_PATTERNS = ( + ("pre_release_region", re.compile(r"\bpre[\W_]*online\b", re.IGNORECASE)), + ("pre_release_region_zh", re.compile(r"\u9884\u53d1")), + ( + "private_icp_endpoint", + re.compile( + r"\b(?!(?:aicp[.-](?:inner|internal)[.-]api[.-]ksyun[.-]com|aicp[.-]api[.-]ksyun[.-]com)\b)" + r"\w*icp[.-](?:inner|internal)[.-]api[.-][\w.-]+\b", + re.IGNORECASE, + ), + ), + ("private_agent_api_endpoint", re.compile(r"\bagent[.-]api[.-]pre\b", re.IGNORECASE)), + ("private_kspmas_endpoint", re.compile(r"\bkspmas[.-]internal\b", re.IGNORECASE)), + ("private_region_header", re.compile(r"\bX[-_]K(?:sc|SC)[-_]Region\b")), + ("private_custom_source_header", re.compile(r"\bX[-_]KSC[-_]CUSTOM[-_]SOURCE\b")), + ( + "private_review_process", + re.compile( + r"\b(?:internal\s+(?:ezone|review\s+gate|maintainer\s+review)|company\s+review)\b", + re.IGNORECASE, + ), + ), + ("private_review_process_zh", re.compile(r"\u5185\u90e8\s*(?:ezone|review|\u5ba1\u6838)")), +) + + +def _assert_no_public_sensitive_patterns(relative_path: str, text: str) -> None: + for label, pattern in PUBLIC_FORBIDDEN_PATTERNS: + assert not pattern.search(text), f"{relative_path} matches {label}: {pattern.pattern}" + + +def test_readmes_position_ksadk_as_runtime_platform(): + expected_sections = ( + "简体中文(默认)", + "一次构建 Agent,到处运行。", + "Agent Runtime Platform", + "public-docs/assets/ksadk-runtime-platform-hero-wide.png", + "真实 CLI 截图", + "为什么需要 KsADK", + "30 秒快速体验", + "真实本地 Web UI 演示", + "public-docs/assets/ksadk-web-ui-screenshot.png", + "public-docs/assets/ksadk-local-debugging-demo.gif", + "public-docs/assets/ksadk-runtime-architecture.png", + "架构", + "生态定位对比", + "可观测", + "文档与样例", + "相关项目", + "参与贡献", + ) + for relative_path in ("README.md", "README.zh-CN.md"): + text = _read(relative_path) + for expected in expected_sections: + assert expected in text, f"{relative_path} missing {expected}" + assert "```mermaid" not in text + assert "Agent Development Kit" not in text + _assert_no_public_sensitive_patterns(relative_path, text) + assert "当前版本:" not in text + assert "候选版本:" not in text + assert "## 0.6.4 重点" not in text + assert "## 0.6.3 重点" not in text + assert "repair_markdown" not in text + assert "[CHANGELOG.md](CHANGELOG.md)" not in text + assert "https://github.com/kingsoftcloud/ksadk-python/releases" not in text + + +def test_english_readme_positions_ksadk_as_runtime_platform(): + text = _read("README.en.md") + expected_sections = ( + "Build agents once. Run them anywhere.", + "Agent Runtime Platform", + "public-docs/assets/ksadk-runtime-platform-hero-wide.png", + "Real KsADK CLI screenshot", + "Why KsADK", + "30 Seconds Quick Start", + "local debugging Web UI", + "public-docs/assets/ksadk-web-ui-screenshot.png", + "public-docs/assets/ksadk-local-debugging-demo.gif", + "public-docs/assets/ksadk-runtime-architecture.png", + "Architecture", + "Ecosystem Positioning", + "Observability", + "Docs And Examples", + "Related Projects", + "Contributing", + ) + for expected in expected_sections: + assert expected in text + assert "```mermaid" not in text + assert "Agent Development Kit" not in text + _assert_no_public_sensitive_patterns("README.en.md", text) + assert "Current version:" not in text + assert "Candidate version:" not in text + assert "## 0.6.4 Highlights" not in text + assert "## 0.6.3 Highlights" not in text + assert "repair_markdown" not in text + assert "[CHANGELOG.md](CHANGELOG.md)" not in text + assert "https://github.com/kingsoftcloud/ksadk-python/releases" not in text + + +def test_docs_homepage_uses_runtime_platform_information_architecture(): + zh = _read("public-docs/index.md") + en = _read("public-docs/index.en.md") + + for expected in ( + "一次构建 Agent,到处运行。", + "Agent Runtime Platform", + "assets/ksadk-runtime-platform-hero-wide.png", + "真实 CLI 截图", + "为什么需要 KsADK", + "真实本地 Web UI 演示", + "assets/ksadk-web-ui-screenshot.png", + "assets/ksadk-local-debugging-demo.gif", + "assets/ksadk-runtime-architecture.png", + "生态定位", + "VEADK", + "AgentRun", + "OpenTelemetry", + "Hermes", + "OpenClaw", + "KSYUN_REGION=cn-beijing-6", + ): + assert expected in zh + + for expected in ( + "Build agents once. Run them anywhere.", + "Agent Runtime Platform", + "assets/ksadk-runtime-platform-hero-wide.png", + "Real KsADK CLI screenshot", + "Why KsADK", + "real local Web UI", + "assets/ksadk-web-ui-screenshot.png", + "assets/ksadk-local-debugging-demo.gif", + "assets/ksadk-runtime-architecture.png", + "Ecosystem Positioning", + "VEADK", + "AgentRun", + "OpenTelemetry", + "Hermes", + "OpenClaw", + "KSYUN_REGION=cn-beijing-6", + ): + assert expected in en + + for text in (zh, en): + assert "Agent Development Kit" not in text + assert "成熟 Agent SDK" not in text + + +def test_docs_include_phase_one_positioning_pages(): + zh_pages = { + "public-docs/getting-started/why-ksadk.md": ( + "为什么需要 KsADK", + "一次构建 Agent,到处运行。", + "ADK 解决 Agent 开发", + "LangGraph", + "OpenAI Agents SDK", + "Agent Runtime Platform", + ), + "public-docs/getting-started/architecture.md": ( + "架构", + "KsADK Agent Runtime Platform 架构", + "Skill Runtime", + "Workspace", + "Sandbox", + "Hermes / OpenClaw Runtime", + ), + "public-docs/getting-started/comparison.md": ( + "生态定位对比", + "这页不是能力打分榜", + "VEADK", + "AgentRun", + "repair_markdown(text, enabled=True)", + ), + } + en_pages = { + "public-docs/getting-started/why-ksadk.en.md": ( + "Why KsADK", + "Build agents once. Run them anywhere.", + "How do I run, debug, expose, deploy, and observe agents consistently?", + "OpenAI Agents SDK", + "Agent Runtime Platform", + ), + "public-docs/getting-started/architecture.en.md": ( + "Architecture", + "KsADK Agent Runtime Platform architecture", + "Skill Runtime", + "Workspace", + "Sandbox", + "Hermes / OpenClaw Runtime", + ), + "public-docs/getting-started/comparison.en.md": ( + "Ecosystem Positioning", + "not a feature scorecard", + "VEADK", + "AgentRun", + "repair_markdown(text, enabled=True)", + ), + } + + for relative_path, expected_terms in {**zh_pages, **en_pages}.items(): + text = _read(relative_path) + for expected in expected_terms: + assert expected in text, f"{relative_path} missing {expected}" + _assert_no_public_sensitive_patterns(relative_path, text) + + +def test_public_positioning_does_not_use_misleading_feature_scorecards(): + scorecard_headers = ( + "| 能力 | ADK | LangGraph | OpenAI Agents SDK | KsADK |", + "| Capability | ADK | LangGraph | OpenAI Agents SDK | KsADK |", + ) + misleading_cells = ( + "| OpenAI 兼容 API | 不内置 | 不内置 | 部分支持 | 支持 |", + "| OpenAI Compatible API | No | No | Partial | Yes |", + ) + + for relative_path in ( + "README.md", + "README.zh-CN.md", + "README.en.md", + "public-docs/index.md", + "public-docs/index.en.md", + "public-docs/getting-started/comparison.md", + "public-docs/getting-started/comparison.en.md", + ): + text = _read(relative_path) + for header in scorecard_headers: + assert header not in text, f"{relative_path} still uses old scorecard header" + for cell in misleading_cells: + assert cell not in text, f"{relative_path} still uses misleading OpenAI comparison" + + for relative_path in ( + "public-docs/index.md", + "public-docs/index.en.md", + "public-docs/getting-started/comparison.md", + "public-docs/getting-started/comparison.en.md", + ): + text = _read(relative_path) + assert "VEADK" in text + assert "AgentRun" in text + + +def test_readmes_stay_concise_and_do_not_duplicate_changelog(): + version_heading_pattern = re.compile(r"^##\s+0\.\d+\.\d+", re.MULTILINE) + + for relative_path in ("README.md", "README.zh-CN.md", "README.en.md"): + text = _read(relative_path) + assert not version_heading_pattern.search(text), ( + f"{relative_path} should link to CHANGELOG/Releases instead of listing version highlights" + ) + assert "GitHub Releases" not in text + assert "[CHANGELOG.md](CHANGELOG.md)" not in text + assert "repair_markdown" not in text + assert "最新" not in text + assert len(text.splitlines()) < 200, f"{relative_path} should stay concise" + assert '

KsADK

' in text + assert '

' in text + assert 'width="860"' in text + assert "ksadk-runtime-platform-hero-wide.png" in text + + +def test_public_positioning_uses_factual_ecosystem_focus_terms(): + expected_terms_by_path = { + "public-docs/getting-started/comparison.md": ("A2UI/Frontend", "VeFaaS", "AgentRuntime 生命周期", "Serverless Devs"), + "public-docs/getting-started/comparison.en.md": ("A2UI/Frontend", "VeFaaS", "AgentRuntime lifecycle", "Serverless Devs"), + } + + for relative_path, expected_terms in expected_terms_by_path.items(): + text = _read(relative_path) + for expected in expected_terms: + assert expected in text, f"{relative_path} missing ecosystem evidence term: {expected}" + + +def test_public_visual_assets_are_present_and_nonempty(): + expected_assets = ( + "public-docs/assets/ksadk-runtime-platform-hero.png", + "public-docs/assets/ksadk-runtime-platform-hero-wide.png", + "public-docs/assets/ksadk-web-ui-screenshot.png", + "public-docs/assets/ksadk-runtime-architecture.svg", + "public-docs/assets/ksadk-runtime-architecture.png", + "public-docs/assets/ksadk-local-debugging-demo.gif", + ) + for relative_path in expected_assets: + path = ROOT / relative_path + assert path.is_file(), f"{relative_path} missing" + assert path.stat().st_size > 4096, f"{relative_path} is unexpectedly small" + + +def test_readme_image_links_resolve_inside_repository(): + markdown_image = re.compile(r"!\[[^\]]*\]\(([^)]+)\)") + html_image = re.compile(r']*\bsrc="([^"]+)"', re.IGNORECASE) + + for relative_markdown_path in ("README.md", "README.zh-CN.md", "README.en.md"): + text = _read(relative_markdown_path) + image_targets = markdown_image.findall(text) + html_image.findall(text) + assert image_targets, f"{relative_markdown_path} should contain rendered images" + for image_target in image_targets: + if "://" in image_target: + continue + image_path = (ROOT / image_target).resolve() + assert image_path.is_relative_to(ROOT), ( + f"{relative_markdown_path} image escapes repository: {image_target}" + ) + assert image_path.is_file(), ( + f"{relative_markdown_path} image target missing: {image_target}" + ) + assert image_path.stat().st_size > 4096, ( + f"{relative_markdown_path} image target unexpectedly small: {image_target}" + ) + + +def test_public_navigation_is_task_oriented(): + mkdocs = _read("mkdocs.yml") + for expected in ("Getting Started", "Build", "Run", "Deploy", "Observe", "Extend", "Reference"): + assert expected in mkdocs + for expected in ( + "为什么需要 KsADK: getting-started/why-ksadk.md", + "架构: getting-started/architecture.md", + "生态定位对比: getting-started/comparison.md", + "为什么需要 KsADK: Why KsADK", + "架构: Architecture", + "生态定位对比: Ecosystem Positioning", + ): + assert expected in mkdocs + assert "快速开始: Quick Start" in mkdocs + assert "Kingsoft Cloud Agent Development Kit" not in mkdocs + assert "金山云智能体开发套件" not in mkdocs + + +def test_markdown_repair_is_documented_as_opt_in(): + expected_by_path = { + "public-docs/guides/agent-best-practices.md": ( + "Markdown 输出修复", + "显式开启", + "repair_markdown(text, enabled=True)", + "默认不会改写模型原文", + ), + "public-docs/guides/agent-best-practices.en.md": ( + "Markdown Output Repair", + "enable the lightweight repair helper", + "repair_markdown(text, enabled=True)", + "does not rewrite raw model output by default", + ), + } + + for relative_path, expected_terms in expected_by_path.items(): + text = _read(relative_path) + for expected in expected_terms: + assert expected in text, f"{relative_path} missing {expected}" + + +def test_english_navigation_translates_all_chinese_labels(): + config = yaml.load(_read("mkdocs.yml"), Loader=MkdocsTestLoader) + translations = ( + config["plugins"][1]["i18n"]["languages"][1]["nav_translations"] + ) + + def iter_labels(items): + for item in items: + if isinstance(item, str): + yield Path(item).stem + elif isinstance(item, dict): + for label, children in item.items(): + yield str(label) + if isinstance(children, list): + yield from iter_labels(children) + + missing = sorted( + label + for label in iter_labels(config["nav"]) + if any("\u4e00" <= character <= "\u9fff" for character in label) + and label not in translations + ) + + assert missing == [] + + +def test_public_materials_do_not_publish_environment_specific_release_words(): + for path in _public_markdown_and_config_files(): + text = path.read_text(encoding="utf-8") + relative_path = path.relative_to(ROOT) + _assert_no_public_sensitive_patterns(str(relative_path), text) + + +def test_package_metadata_is_runtime_platform_positioned_for_patch_candidate(): + pyproject = tomllib.loads(_read("pyproject.toml")) + init_text = _read("ksadk/__init__.py") + version_text = _read("ksadk/version.py") + + assert pyproject["project"]["version"] == "0.6.4" + assert 'VERSION = "0.6.4"' in version_text + assert "Agent Runtime Platform" in pyproject["project"]["description"] + assert "Agent Runtime Platform" in init_text + assert "Agent Development Kit" not in pyproject["project"]["description"] + assert "Agent Development Kit" not in init_text + + +def test_patch_version_changelog_is_ready_for_authorized_release(): + changelog = _read("CHANGELOG.md") + assert "## [0.6.4] - 2026-06-10" in changelog + assert "## [0.6.4] - Unreleased" not in changelog + assert "用户 review 通过前" not in changelog + assert "不创建 tag" not in changelog + assert "不发布 GitHub Release" not in changelog + assert "不上传 PyPI" not in changelog diff --git a/tests/test_public_release_gates.py b/tests/test_public_release_gates.py new file mode 100644 index 0000000..ac5149b --- /dev/null +++ b/tests/test_public_release_gates.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _makefile() -> str: + return (ROOT / "Makefile").read_text(encoding="utf-8") + + +def _target_dependencies(makefile: str, target: str) -> set[str]: + match = re.search(rf"^{re.escape(target)}:\s*(?P[^\n]*)$", makefile, re.MULTILINE) + assert match, f"missing Makefile target: {target}" + return set(match.group("deps").split()) + + +def test_external_publish_targets_require_review_gate_and_publish_state_check(): + makefile = _makefile() + + for target in ("publish", "publish-test"): + deps = _target_dependencies(makefile, target) + assert "open-source-approval-check" in deps + assert "public-preflight" in deps + assert "public-publish-check" in deps + + +def test_public_release_tag_requires_approval_check(): + makefile = _makefile() + + deps = _target_dependencies(makefile, "public-release-tag") + + assert "open-source-approval-check" in deps + assert "public-preflight" in deps + assert "public-publish-check" in deps + assert "内部审核" not in makefile + + +def test_publication_state_make_target_uses_valid_phase(): + makefile = _makefile() + + match = re.search( + r"^open-source-publication-state:\n(?P(?:\t.*\n)+)", + makefile, + re.MULTILINE, + ) + assert match + body = match.group("body") + + assert "--phase placeholder" not in body + assert "--phase pre-publish" in body or 'PUBLIC_PUBLISH_PHASE' in body + + +def test_public_test_and_ci_cover_release_gate_and_runtime_markdown_tests(): + makefile = _makefile() + ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + + for required in ( + "tests/test_check_approval_record.py", + "tests/test_public_release_gates.py", + "tests/test_markdown_repair.py", + "tests/test_conversation_runtime.py", + "tests/test_server_session_app.py", + ): + assert required in makefile + assert required in ci diff --git a/tests/test_server_session_app.py b/tests/test_server_session_app.py index 179b219..7fe09db 100644 --- a/tests/test_server_session_app.py +++ b/tests/test_server_session_app.py @@ -991,12 +991,14 @@ async def test_chat_completions_forwards_model_to_runner(monkeypatch): "messages": [{"role": "user", "content": "hello"}], "stream": False, "model": "glm-5.1", + "account_id": "acct-chat", }, ) assert response.status_code == 200 assert runner.prepared_models == ["glm-5.1"] assert runner.calls[-1]["model"] == "glm-5.1" + assert runner.calls[-1]["platform_context"]["account_id"] == "acct-chat" @pytest.mark.asyncio @@ -1479,6 +1481,7 @@ async def test_responses_uses_official_conversation_as_runtime_session(monkeypat "input": "hello", "conversation": "conv-a", "safety_identifier": "user-a", + "account_id": "acct-a", "stream": False, }, ) @@ -1491,6 +1494,7 @@ async def test_responses_uses_official_conversation_as_runtime_session(monkeypat assert session.user_id == "user-a" assert runner.calls[-1]["session_id"] == "conv-a" assert runner.calls[-1]["platform_context"]["user_id"] == "user-a" + assert runner.calls[-1]["platform_context"]["account_id"] == "acct-a" @pytest.mark.asyncio diff --git a/uv.lock b/uv.lock index 9d458ec..be2e353 100644 --- a/uv.lock +++ b/uv.lock @@ -2451,7 +2451,7 @@ wheels = [ [[package]] name = "ksadk" -version = "0.6.3" +version = "0.6.4" source = { editable = "." } dependencies = [ { name = "a2a-sdk" },