diff --git a/.gitignore b/.gitignore index 56137961..7d58a97e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Thumbs.db __pycache__/ *.pyc +.env +.env.* +!.env.example diff --git a/issue2/README.en.md b/issue2/README.en.md new file mode 100644 index 00000000..a604e80d --- /dev/null +++ b/issue2/README.en.md @@ -0,0 +1,23 @@ +# Hy3 Integrations and Evidence Board + +This directory implements [Issue #2](https://github.com/Tencent-Hunyuan/Hy3/issues/2): + +- [Five end-user integration guides](integrations/README.en.md) +- [Hy3 Evidence Board](demo/README.en.md), a small web app that keeps API keys on the server and uses Hy3 tool calling to search a local knowledge base before writing an evidence-backed report + +![Eight-second offline Evidence Board walkthrough](assets/evidence-board-demo.gif) + +> 中文:[README.md](README.md) + +## Verification boundary + +Configuration fields and commands were checked against each product's official documentation. Offline mode, tests, and the web UI run without an API key. A real Hy3 request requires either an OpenRouter key or a reachable self-hosted OpenAI-compatible Hy3 endpoint. Offline output is visibly marked and never presented as model output. + +## Layout + +```text +issue2/ +├── integrations/ # Bilingual integration guides +├── demo/ # Standalone-ready showcase source +└── assets/ # Locally captured screenshots and demo media +``` diff --git a/issue2/README.md b/issue2/README.md new file mode 100644 index 00000000..8a170359 --- /dev/null +++ b/issue2/README.md @@ -0,0 +1,23 @@ +# Hy3 主流工具集成与 Evidence Board + +本目录对应 [Issue #2](https://github.com/Tencent-Hunyuan/Hy3/issues/2),包含: + +- [5 个主流工具的 Hy3 接入指南](integrations/README.md) +- [Hy3 Evidence Board 小作品](demo/README.md):一个以服务端保管密钥、用 Hy3 工具调用检索本地资料并生成带证据报告的 Web 应用 + +![Evidence Board 8 秒离线交互演示](assets/evidence-board-demo.gif) + +> English: [README.en.md](README.en.md) + +## 验证边界 + +文档中的字段和命令已按各工具官方文档核对;仓库内的离线模式、单元测试和 Web 页面可以在没有 API Key 的环境运行。真实 Hy3 请求需要自行提供 OpenRouter Key 或可访问的自建 Hy3 OpenAI 兼容端点。离线模式会在界面显著标注,不会冒充真实模型输出。 + +## 目录 + +```text +issue2/ +├── integrations/ # 中英双语接入指南 +├── demo/ # 可独立发布的小作品源码 +└── assets/ # 本地运行所得截图与演示素材 +``` diff --git a/issue2/assets/evidence-board-demo.gif b/issue2/assets/evidence-board-demo.gif new file mode 100644 index 00000000..f6826786 Binary files /dev/null and b/issue2/assets/evidence-board-demo.gif differ diff --git a/issue2/assets/evidence-board-mobile.png b/issue2/assets/evidence-board-mobile.png new file mode 100644 index 00000000..e9bc5315 Binary files /dev/null and b/issue2/assets/evidence-board-mobile.png differ diff --git a/issue2/assets/evidence-board-offline.png b/issue2/assets/evidence-board-offline.png new file mode 100644 index 00000000..a011461d Binary files /dev/null and b/issue2/assets/evidence-board-offline.png differ diff --git a/issue2/demo/.env.example b/issue2/demo/.env.example new file mode 100644 index 00000000..f3950447 --- /dev/null +++ b/issue2/demo/.env.example @@ -0,0 +1,9 @@ +# Choose: openrouter or selfhost +HY3_PROVIDER=openrouter +HY3_BASE_URL=https://openrouter.ai/api/v1 +HY3_MODEL=tencent/hy3 +HY3_API_KEY=replace-me +HY3_REASONING_EFFORT=low + +# For a clearly labeled offline UI/retrieval demo: +# HY3_DEMO_MODE=1 diff --git a/issue2/demo/LICENSE b/issue2/demo/LICENSE new file mode 100644 index 00000000..eb04ae97 --- /dev/null +++ b/issue2/demo/LICENSE @@ -0,0 +1,210 @@ +Tencent is pleased to support the open source community by making Tencent Hy3 available. + +Copyright (C) 2026 Tencent. All rights reserved. + +Tencent Hy3 is licensed under the Apache-2.0. + + +Terms of the Apache-2.0: +-------------------------------------------------------------------- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/issue2/demo/README.en.md b/issue2/demo/README.en.md new file mode 100644 index 00000000..026fff0b --- /dev/null +++ b/issue2/demo/README.en.md @@ -0,0 +1,64 @@ +# Hy3 Evidence Board + +> 中文:[README.md](README.md) + +Evidence Board is a small Hy3 agent application. The model must call `search_knowledge_base`; the server performs a read-only search and returns evidence for a cited final report. + +![Eight-second offline walkthrough](../assets/evidence-board-demo.gif) + +## Hy3 capabilities + +- OpenAI-compatible tool calling +- A grounded multi-turn agent loop +- `no_think / low / high` reasoning mapping +- Long-form synthesis with source IDs + +## Security + +- The API key stays in server environment variables and never reaches browser storage. +- The only tool is read-only local search; the model cannot run a shell or read arbitrary files. +- Tool calls are capped at three rounds, requests at 32 KiB, and questions at 10–500 characters. +- Offline `DemoProvider` output is clearly labeled and never impersonates Hy3. + +## Run + +Offline: + +```bash +cd issue2/demo +HY3_DEMO_MODE=1 python3 server.py +``` + +Open . + +OpenRouter live mode: + +```bash +export HY3_PROVIDER=openrouter +export HY3_API_KEY='sk-or-v1-...' +export HY3_MODEL=tencent/hy3 +python3 server.py +``` + +Self-hosted mode: + +```bash +export HY3_PROVIDER=selfhost +export HY3_BASE_URL=http://127.0.0.1:8000/v1 +export HY3_API_KEY=EMPTY +export HY3_MODEL=hy3 +python3 server.py +``` + +Enable Hy3's appropriate tool-call parser on the server. + +## Verify + +```bash +python3 -m unittest discover -s tests -v +python3 server.py --check +``` + +`--check` is deterministic, ignores API credentials, and sends no network request. + +This directory has no dependency on a Hy3 repository package and is ready to copy into a standalone repository. Remote publication and a live demo video remain separate authorized actions. diff --git a/issue2/demo/README.md b/issue2/demo/README.md new file mode 100644 index 00000000..e4a91918 --- /dev/null +++ b/issue2/demo/README.md @@ -0,0 +1,78 @@ +# Hy3 Evidence Board + +> English: [README.en.md](README.en.md) + +Evidence Board 是一个小而完整的 Hy3 Agent 应用:模型必须先调用 `search_knowledge_base`,服务端执行只读检索,再把证据回传给模型生成带来源报告。 + +![8 秒离线交互演示](../assets/evidence-board-demo.gif) + +## 展示的 Hy3 核心能力 + +- OpenAI-compatible tool calling +- 基于证据的多轮 Agent 循环 +- `no_think / low / high` 推理强度映射 +- 长文整理与来源标注 + +## 安全设计 + +- API Key 只从服务端环境变量读取,不传给浏览器、不写 `localStorage`。 +- 唯一工具是只读的本地知识库检索,模型不能执行 shell 或任意文件读取。 +- 最多 3 轮工具调用,请求体限制 32 KiB,问题限制 10~500 字。 +- 离线模式使用确定性 `DemoProvider`,页面和响应均明确标注“未调用 Hy3”。 + +## 运行 + +### 离线模式(无需 Key) + +```bash +cd issue2/demo +HY3_DEMO_MODE=1 python3 server.py +``` + +打开 。 + +### OpenRouter 实时 Hy3 + +```bash +export HY3_PROVIDER=openrouter +export HY3_API_KEY='sk-or-v1-...' +export HY3_MODEL=tencent/hy3 +python3 server.py +``` + +### 自建 vLLM/SGLang + +```bash +export HY3_PROVIDER=selfhost +export HY3_BASE_URL=http://127.0.0.1:8000/v1 +export HY3_API_KEY=EMPTY +export HY3_MODEL=hy3 +python3 server.py +``` + +自建服务必须按 Hy3 README 启用对应的 tool-call parser。 + +## 验证 + +```bash +python3 -m unittest discover -s tests -v +python3 server.py --check +``` + +`--check` 只检查离线工具链,不读取环境中的 Key,也不发网络请求。 + +## 目录 + +```text +demo/ +├── evidence_board.py # 检索、Provider 与 Agent 循环 +├── server.py # 标准库 HTTP 服务/API +├── static/ # HTML/CSS/JS +├── knowledge/ # 带原始 URL 的本地资料 +├── tests/ # 单元与 HTTP 冒烟测试 +└── .env.example # 变量名示例,不含真实 Key +``` + +## 独立仓库交付 + +本目录不依赖 Hy3 主仓库中的 Python 包,可直接复制为独立仓库并使用 Apache-2.0 兼容许可发布。当前任务未获授权创建远程仓库或上传视频,因此只准备可独立发布的源码;发布后在集成索引中替换为远程地址,并录制 ≤1 分钟的实时 Hy3 演示。 diff --git a/issue2/demo/evidence_board.py b/issue2/demo/evidence_board.py new file mode 100644 index 00000000..ab73c660 --- /dev/null +++ b/issue2/demo/evidence_board.py @@ -0,0 +1,326 @@ +"""Core retrieval and Hy3 tool-calling loop for Evidence Board.""" + +from __future__ import annotations + +import json +import os +import re +import urllib.error +import urllib.request +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + + +QUESTION_MIN_LENGTH = 10 +QUESTION_MAX_LENGTH = 500 +SEARCH_TOOL = { + "type": "function", + "function": { + "name": "search_knowledge_base", + "description": "Search the bundled Hy3 knowledge base and return grounded excerpts.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A focused search query derived from the user's question.", + } + }, + "required": ["query"], + "additionalProperties": False, + }, + }, +} + + +class EvidenceBoardError(Exception): + """Base exception with a user-safe message.""" + + +class ValidationError(EvidenceBoardError): + """Raised when user input is invalid.""" + + +class ProviderError(EvidenceBoardError): + """Raised when the upstream model API fails.""" + + +@dataclass(frozen=True) +class Source: + source_id: str + title: str + path: str + url: str + content: str + + def public(self, excerpt: str) -> dict[str, str]: + return { + "id": self.source_id, + "title": self.title, + "path": self.path, + "url": self.url, + "excerpt": excerpt, + } + + +def normalize_question(value: Any) -> str: + if not isinstance(value, str): + raise ValidationError("问题必须是字符串。") + question = " ".join(value.split()) + if len(question) < QUESTION_MIN_LENGTH: + raise ValidationError(f"问题至少需要 {QUESTION_MIN_LENGTH} 个字符。") + if len(question) > QUESTION_MAX_LENGTH: + raise ValidationError(f"问题不能超过 {QUESTION_MAX_LENGTH} 个字符。") + return question + + +def tokenize(text: str) -> list[str]: + """Tokenize ASCII words plus individual CJK characters without dependencies.""" + return re.findall(r"[a-z0-9_]+|[\u3400-\u9fff]", text.lower()) + + +class KnowledgeBase: + def __init__(self, sources: list[Source]): + if not sources: + raise ValueError("knowledge base must contain at least one source") + self.sources = sources + + @classmethod + def from_directory(cls, root: Path) -> "KnowledgeBase": + sources: list[Source] = [] + for path in sorted(root.glob("*.md")): + raw = path.read_text(encoding="utf-8") + title_match = re.search(r"^#\s+(.+)$", raw, re.MULTILINE) + url_match = re.search(r"^Source:\s*(https?://\S+)\s*$", raw, re.MULTILINE) + sources.append( + Source( + source_id=path.stem, + title=title_match.group(1).strip() if title_match else path.stem, + path=f"knowledge/{path.name}", + url=url_match.group(1) if url_match else "", + content=raw, + ) + ) + return cls(sources) + + def search(self, query: str, limit: int = 4) -> list[dict[str, str]]: + query_tokens = set(tokenize(query)) + if not query_tokens: + return [] + ranked: list[tuple[int, str, Source, str]] = [] + for source in self.sources: + best_score = 0 + best_excerpt = "" + paragraphs = [p.strip() for p in re.split(r"\n\s*\n", source.content) if p.strip()] + for paragraph in paragraphs: + paragraph_tokens = tokenize(paragraph) + overlap = sum(1 for token in paragraph_tokens if token in query_tokens) + unique_overlap = len(query_tokens.intersection(paragraph_tokens)) + score = overlap + unique_overlap * 3 + if score > best_score: + best_score = score + best_excerpt = re.sub(r"\s+", " ", paragraph)[:700] + if best_score: + ranked.append((best_score, source.source_id, source, best_excerpt)) + ranked.sort(key=lambda row: (-row[0], row[1])) + return [source.public(excerpt) for _, _, source, excerpt in ranked[:limit]] + + +class ChatProvider(Protocol): + mode: str + + def complete(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> dict[str, Any]: + """Return an OpenAI-compatible assistant message.""" + + +class OpenAICompatibleProvider: + mode = "live" + + def __init__( + self, + *, + base_url: str, + api_key: str, + model: str, + provider_kind: str = "selfhost", + reasoning_effort: str = "low", + timeout: float = 120.0, + ): + if not base_url or not api_key or not model: + raise ValueError("base_url, api_key, and model are required") + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.model = model + self.provider_kind = provider_kind + self.reasoning_effort = reasoning_effort + self.timeout = timeout + + def build_payload(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> dict[str, Any]: + payload: dict[str, Any] = { + "model": self.model, + "messages": messages, + "tools": tools, + "tool_choice": "auto", + "temperature": 0.9, + "top_p": 1.0, + "max_tokens": 4096, + } + if self.provider_kind == "openrouter": + payload["reasoning"] = {"effort": self.reasoning_effort} + else: + payload["chat_template_kwargs"] = {"reasoning_effort": self.reasoning_effort} + return payload + + def complete(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> dict[str, Any]: + payload = self.build_payload(messages, tools) + request = urllib.request.Request( + f"{self.base_url}/chat/completions", + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "User-Agent": "hy3-evidence-board/1.0", + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=self.timeout) as response: + data = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace")[:500] + raise ProviderError(f"Hy3 API 返回 HTTP {exc.code}: {detail}") from exc + except (urllib.error.URLError, TimeoutError) as exc: + raise ProviderError(f"无法连接 Hy3 API: {exc}") from exc + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ProviderError("Hy3 API 返回了无法解析的响应。") from exc + try: + return data["choices"][0]["message"] + except (KeyError, IndexError, TypeError) as exc: + raise ProviderError("Hy3 API 响应缺少 choices[0].message。") from exc + + +class DemoProvider: + """Deterministic provider for offline tests; never impersonates Hy3.""" + + mode = "demo" + + def complete(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]]) -> dict[str, Any]: + tool_messages = [message for message in messages if message.get("role") == "tool"] + if not tool_messages: + question = next(message["content"] for message in reversed(messages) if message.get("role") == "user") + return { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": f"demo_{uuid.uuid4().hex[:8]}", + "type": "function", + "function": { + "name": "search_knowledge_base", + "arguments": json.dumps({"query": question}, ensure_ascii=False), + }, + } + ], + } + evidence = json.loads(tool_messages[-1]["content"]) + lines = [ + "## 离线链路检查结果", + "", + "> 此内容由确定性 DemoProvider 生成,未调用 Hy3。", + "", + ] + for item in evidence: + lines.append(f"- **{item['title']}**:{item['excerpt']} [{item['id']}]") + lines.append("") + lines.append("配置真实端点后,Hy3 会基于同一批工具证据生成正式报告。") + return {"role": "assistant", "content": "\n".join(lines)} + + +class ResearchAgent: + def __init__(self, provider: ChatProvider, knowledge_base: KnowledgeBase, max_rounds: int = 3): + self.provider = provider + self.knowledge_base = knowledge_base + self.max_rounds = max_rounds + + def run(self, raw_question: Any) -> dict[str, Any]: + question = normalize_question(raw_question) + messages: list[dict[str, Any]] = [ + { + "role": "system", + "content": ( + "你是严谨的 Hy3 资料研究助手。必须先调用 search_knowledge_base;" + "只根据工具返回的材料回答,每条关键结论以 [source_id] 标注。" + "证据不足时明确说明,不得编造。" + ), + }, + {"role": "user", "content": question}, + ] + trace: list[dict[str, Any]] = [] + evidence_by_id: dict[str, dict[str, str]] = {} + for round_number in range(1, self.max_rounds + 1): + assistant = self.provider.complete(messages, [SEARCH_TOOL]) + tool_calls = assistant.get("tool_calls") or [] + if not tool_calls: + content = assistant.get("content") + if not isinstance(content, str) or not content.strip(): + raise ProviderError("模型既未返回文本,也未请求工具。") + return { + "mode": self.provider.mode, + "question": question, + "answer": content, + "evidence": list(evidence_by_id.values()), + "trace": trace, + } + messages.append(assistant) + for call in tool_calls: + function = call.get("function") or {} + if function.get("name") != "search_knowledge_base": + raise ProviderError(f"模型请求了未授权工具: {function.get('name', '')}") + try: + arguments = json.loads(function.get("arguments") or "{}") + except json.JSONDecodeError as exc: + raise ProviderError("工具参数不是有效 JSON。") from exc + query = arguments.get("query") + if not isinstance(query, str) or not query.strip(): + raise ProviderError("search_knowledge_base 缺少 query。") + results = self.knowledge_base.search(query) + for item in results: + evidence_by_id[item["id"]] = item + trace.append( + { + "round": round_number, + "tool": "search_knowledge_base", + "query": query, + "result_count": len(results), + } + ) + messages.append( + { + "role": "tool", + "tool_call_id": call.get("id", f"call_{round_number}"), + "content": json.dumps(results, ensure_ascii=False), + } + ) + raise ProviderError("达到最大工具调用轮数,仍未得到最终回答。") + + +def provider_from_environment() -> ChatProvider: + if os.getenv("HY3_DEMO_MODE", "").lower() in {"1", "true", "yes"}: + return DemoProvider() + provider_kind = os.getenv("HY3_PROVIDER", "selfhost").lower() + defaults = { + "openrouter": ("https://openrouter.ai/api/v1", "tencent/hy3"), + "selfhost": ("http://127.0.0.1:8000/v1", "hy3"), + } + if provider_kind not in defaults: + raise ValueError("HY3_PROVIDER must be 'openrouter' or 'selfhost'") + default_url, default_model = defaults[provider_kind] + return OpenAICompatibleProvider( + base_url=os.getenv("HY3_BASE_URL", default_url), + api_key=os.getenv("HY3_API_KEY", ""), + model=os.getenv("HY3_MODEL", default_model), + provider_kind=provider_kind, + reasoning_effort=os.getenv("HY3_REASONING_EFFORT", "low"), + ) diff --git a/issue2/demo/knowledge/model.md b/issue2/demo/knowledge/model.md new file mode 100644 index 00000000..fb37bb8d --- /dev/null +++ b/issue2/demo/knowledge/model.md @@ -0,0 +1,9 @@ +# Hy3 模型概览 + +Source: https://github.com/Tencent-Hunyuan/Hy3/blob/rhinobird2026/README_CN.md + +Hy3 是腾讯混元团队研发的快慢思考融合混合专家模型。总参数量 295B,激活参数 21B,MTP 层参数 3.8B;共有 192 个专家,每次 top-8 激活。 + +模型上下文长度为 256K。它重点增强了推理、智能体、长上下文、多轮意图保持、工具调用和输出格式稳定性。 + +官方推荐参数为 temperature=0.9、top_p=1.0。reasoning_effort 支持 no_think、low 和 high:复杂数学、编程和推理任务建议 high,日常问题可使用 no_think。 diff --git a/issue2/demo/knowledge/openrouter.md b/issue2/demo/knowledge/openrouter.md new file mode 100644 index 00000000..e8a84ac4 --- /dev/null +++ b/issue2/demo/knowledge/openrouter.md @@ -0,0 +1,9 @@ +# OpenRouter 上的 Hy3 + +Source: https://openrouter.ai/tencent/hy3 + +OpenRouter 的 Hy3 模型 ID 是 `tencent/hy3`,通过 `https://openrouter.ai/api/v1/chat/completions` 使用 OpenAI 兼容请求。 + +模型页列出约 262K token 上下文,并支持 reasoning、reasoning_effort、tools 和 tool_choice 等参数。OpenRouter 的价格、免费路由和供应商状态会变化,使用前应实时查看模型页,不在本地资料中固化价格。 + +API Key 应通过服务端环境变量提供,不应放入浏览器源码、截图或 Git 仓库。 diff --git a/issue2/demo/knowledge/serving.md b/issue2/demo/knowledge/serving.md new file mode 100644 index 00000000..9839a966 --- /dev/null +++ b/issue2/demo/knowledge/serving.md @@ -0,0 +1,9 @@ +# Hy3 推理与 Agent 部署 + +Source: https://github.com/Tencent-Hunyuan/Hy3/blob/rhinobird2026/README_CN.md + +Hy3 可通过 vLLM 或 SGLang 部署成 OpenAI 兼容 API。官方说明 8 卡部署建议使用 H20-3e 或其他具有更大显存的卡型。 + +vLLM 启动 Agent 工具调用时需要 `--tool-call-parser hy_v3`、`--reasoning-parser hy_v3` 和 `--enable-auto-tool-choice`。服务名可以用 `--served-model-name hy3` 固定为 hy3。 + +SGLang 对应使用 `--tool-call-parser hunyuan` 和 `--reasoning-parser hunyuan`。客户端应以实际 served model name 调用,并验证 `/chat/completions` 的真实响应。 diff --git a/issue2/demo/server.py b/issue2/demo/server.py new file mode 100644 index 00000000..8d0cf6b4 --- /dev/null +++ b/issue2/demo/server.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Dependency-free HTTP server for Hy3 Evidence Board.""" + +from __future__ import annotations + +import argparse +import json +import mimetypes +import os +import sys +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urlsplit + +from evidence_board import ( + DemoProvider, + KnowledgeBase, + ProviderError, + ResearchAgent, + ValidationError, + provider_from_environment, +) + + +ROOT = Path(__file__).resolve().parent +STATIC_ROOT = ROOT / "static" +KNOWLEDGE_ROOT = ROOT / "knowledge" +MAX_REQUEST_BYTES = 32 * 1024 + + +def build_agent() -> ResearchAgent: + return ResearchAgent(provider_from_environment(), KnowledgeBase.from_directory(KNOWLEDGE_ROOT)) + + +class EvidenceBoardHandler(BaseHTTPRequestHandler): + server_version = "Hy3EvidenceBoard/1.0" + agent: ResearchAgent + + def log_message(self, format: str, *args: object) -> None: + sys.stderr.write("[%s] %s\n" % (self.log_date_time_string(), format % args)) + + def send_json(self, status: HTTPStatus, payload: dict[str, object]) -> None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status.value) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: + path = urlsplit(self.path).path + if path == "/api/health": + self.send_json( + HTTPStatus.OK, + { + "ok": True, + "mode": self.agent.provider.mode, + "model": getattr(self.agent.provider, "model", "offline-demo"), + }, + ) + return + if path == "/": + path = "/index.html" + relative = Path(path.lstrip("/")) + if any(part in {"..", "."} for part in relative.parts): + self.send_error(HTTPStatus.NOT_FOUND.value) + return + target = (STATIC_ROOT / relative).resolve() + if STATIC_ROOT.resolve() not in target.parents or not target.is_file(): + self.send_error(HTTPStatus.NOT_FOUND.value) + return + body = target.read_bytes() + content_type, _ = mimetypes.guess_type(target.name) + self.send_response(HTTPStatus.OK.value) + self.send_header("Content-Type", content_type or "application/octet-stream") + self.send_header("Content-Length", str(len(body))) + self.send_header("X-Content-Type-Options", "nosniff") + self.end_headers() + self.wfile.write(body) + + def do_POST(self) -> None: + if urlsplit(self.path).path != "/api/research": + self.send_error(HTTPStatus.NOT_FOUND.value) + return + content_type = self.headers.get_content_type() + if content_type != "application/json": + self.send_json(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, {"ok": False, "error": "Content-Type 必须是 application/json。"}) + return + try: + length = int(self.headers.get("Content-Length", "0")) + except ValueError: + self.send_json(HTTPStatus.BAD_REQUEST, {"ok": False, "error": "Content-Length 无效。"}) + return + if length <= 0 or length > MAX_REQUEST_BYTES: + self.send_json(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, {"ok": False, "error": "请求体为空或过大。"}) + return + try: + payload = json.loads(self.rfile.read(length).decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + self.send_json(HTTPStatus.BAD_REQUEST, {"ok": False, "error": "请求体不是有效 JSON。"}) + return + if not isinstance(payload, dict): + self.send_json(HTTPStatus.BAD_REQUEST, {"ok": False, "error": "JSON 顶层必须是对象。"}) + return + try: + result = self.agent.run(payload.get("question")) + except ValidationError as exc: + self.send_json(HTTPStatus.BAD_REQUEST, {"ok": False, "error": str(exc)}) + return + except ProviderError as exc: + self.send_json(HTTPStatus.BAD_GATEWAY, {"ok": False, "error": str(exc)}) + return + self.send_json(HTTPStatus.OK, {"ok": True, **result}) + + +def make_server(host: str, port: int, agent: ResearchAgent | None = None) -> ThreadingHTTPServer: + handler = type("ConfiguredEvidenceBoardHandler", (EvidenceBoardHandler,), {"agent": agent or build_agent()}) + return ThreadingHTTPServer((host, port), handler) + + +def run_check() -> int: + agent = ResearchAgent(DemoProvider(), KnowledgeBase.from_directory(KNOWLEDGE_ROOT)) + result = agent.run("请解释 Hy3 的模型规模、上下文长度以及 Agent 工具调用部署要求。") + if result["mode"] != "demo" or not result["trace"] or not result["evidence"]: + print("self-check failed", file=sys.stderr) + return 1 + print(json.dumps({"ok": True, "mode": result["mode"], "evidence": len(result["evidence"]), "trace": result["trace"]}, ensure_ascii=False)) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run Hy3 Evidence Board") + parser.add_argument("--host", default=os.getenv("HY3_HOST", "127.0.0.1")) + parser.add_argument("--port", type=int, default=int(os.getenv("HY3_PORT", "8765"))) + parser.add_argument("--check", action="store_true", help="run a deterministic offline self-check") + args = parser.parse_args() + if args.check: + return run_check() + try: + server = make_server(args.host, args.port) + except ValueError as exc: + parser.error(str(exc)) + print(f"Hy3 Evidence Board ({server.RequestHandlerClass.agent.provider.mode}) -> http://{args.host}:{args.port}") + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/issue2/demo/static/app.js b/issue2/demo/static/app.js new file mode 100644 index 00000000..64fe893a --- /dev/null +++ b/issue2/demo/static/app.js @@ -0,0 +1,104 @@ +const question = document.querySelector('#question'); +const counter = document.querySelector('#counter'); +const button = document.querySelector('#submitButton'); +const statusBox = document.querySelector('#status'); +const results = document.querySelector('#results'); +const answer = document.querySelector('#answer'); +const trace = document.querySelector('#trace'); +const sources = document.querySelector('#sources'); +const modeBadge = document.querySelector('#modeBadge'); +const offlineNotice = document.querySelector('#offlineNotice'); + +function escapeHtml(value) { + return String(value).replace(/[&<>'"]/g, char => ({ + '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' + })[char]); +} + +function renderMarkdown(value) { + const safe = escapeHtml(value); + const lines = safe.split('\n'); + let inList = false; + const rendered = []; + for (const line of lines) { + if (line.startsWith('## ')) { + if (inList) { rendered.push(''); inList = false; } + rendered.push(`

${line.slice(3)}

`); + } else if (line.startsWith('> ')) { + if (inList) { rendered.push(''); inList = false; } + rendered.push(`
${line.slice(5)}
`); + } else if (line.startsWith('- ')) { + if (!inList) { rendered.push('
    '); inList = true; } + rendered.push(`
  • ${line.slice(2).replace(/\*\*(.+?)\*\*/g, '$1')}
  • `); + } else if (line.trim()) { + if (inList) { rendered.push('
'); inList = false; } + rendered.push(`

${line.replace(/`(.+?)`/g, '$1')}

`); + } + } + if (inList) rendered.push(''); + return rendered.join(''); +} + +function updateCounter() { + counter.textContent = `${question.value.length} / 500`; +} + +async function loadHealth() { + try { + const response = await fetch('/api/health', {cache: 'no-store'}); + const data = await response.json(); + const isDemo = data.mode === 'demo'; + modeBadge.textContent = isDemo ? 'Offline Demo · 未调用 Hy3' : `Live Hy3 · ${data.model}`; + modeBadge.className = `mode-badge ${isDemo ? 'demo' : 'live'}`; + offlineNotice.classList.toggle('hidden', !isDemo); + } catch (error) { + modeBadge.textContent = 'Service unavailable'; + modeBadge.className = 'mode-badge pending'; + } +} + +function renderResult(data) { + answer.innerHTML = renderMarkdown(data.answer); + trace.innerHTML = data.trace.map(item => ` +
  • ${escapeHtml(item.tool)} + Round ${item.round} · ${item.result_count} sources
    ${escapeHtml(item.query)} +
  • `).join(''); + sources.innerHTML = data.evidence.map(item => ` + `).join(''); + results.classList.remove('hidden'); +} + +async function runResearch() { + statusBox.className = 'status'; + statusBox.textContent = '正在请求模型并等待工具调用…'; + statusBox.classList.remove('hidden'); + results.classList.add('hidden'); + button.disabled = true; + try { + const response = await fetch('/api/research', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({question: question.value}) + }); + const data = await response.json(); + if (!response.ok || !data.ok) throw new Error(data.error || `HTTP ${response.status}`); + renderResult(data); + statusBox.textContent = data.mode === 'demo' + ? '离线链路已完成:未调用 Hy3。' + : '实时 Hy3 工具调用与报告已完成。'; + } catch (error) { + statusBox.className = 'status error'; + statusBox.textContent = error.message; + } finally { + button.disabled = false; + } +} + +question.addEventListener('input', updateCounter); +button.addEventListener('click', runResearch); +updateCounter(); +loadHealth(); diff --git a/issue2/demo/static/index.html b/issue2/demo/static/index.html new file mode 100644 index 00000000..5ca62c26 --- /dev/null +++ b/issue2/demo/static/index.html @@ -0,0 +1,73 @@ + + + + + + + Hy3 Evidence Board + + + + +
    +
    +
    +

    TENCENT HY3 · TOOL CALLING SHOWCASE

    +

    Evidence Board

    +

    先检索,再回答。让每条结论都能回到来源。

    +
    +
    检查服务…
    +
    + + + +
    + + + +
    + + + + + +
    + API Key 仅保存在服务端环境变量 + OpenAI-compatible · No browser storage +
    +
    + + + diff --git a/issue2/demo/static/styles.css b/issue2/demo/static/styles.css new file mode 100644 index 00000000..5e862e05 --- /dev/null +++ b/issue2/demo/static/styles.css @@ -0,0 +1,93 @@ +:root { + color-scheme: dark; + --ink: #f7f3e8; + --muted: #aca89d; + --paper: #151513; + --panel: #1d1d1a; + --line: #3b3a34; + --accent: #ff6b35; + --accent-soft: #ffbe99; + --mint: #8be8cb; + --danger: #ff9285; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + min-height: 100vh; + color: var(--ink); + background: + radial-gradient(circle at 82% 4%, rgba(255, 107, 53, .16), transparent 31rem), + linear-gradient(180deg, #0e0e0d 0%, #171714 100%); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + opacity: .18; + background-image: linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px); + background-size: 40px 40px; +} + +.shell { position: relative; width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 64px 0 32px; } +.hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 30px; margin-bottom: 42px; } +.eyebrow { margin: 0 0 16px; color: var(--accent-soft); font: 700 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .18em; } +h1 { margin: 0; max-width: 760px; font: 800 clamp(52px, 9vw, 108px)/.83 Georgia, "Times New Roman", serif; letter-spacing: -.065em; } +.lede { margin: 26px 0 0; color: var(--muted); font-size: clamp(17px, 2vw, 22px); } +.mode-badge { flex: 0 0 auto; padding: 11px 15px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font: 700 12px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .08em; } +.mode-badge.live { border-color: var(--mint); color: var(--mint); } +.mode-badge.demo { border-color: var(--accent); color: var(--accent-soft); } + +.notice { display: flex; gap: 12px; align-items: center; margin: 0 0 18px; padding: 14px 18px; border-left: 4px solid var(--accent); background: rgba(255, 107, 53, .1); color: var(--accent-soft); } +.notice span { color: var(--ink); } +.hidden { display: none !important; } + +.query-card, .report-card, .trace-card, .source-card { border: 1px solid var(--line); background: rgba(29, 29, 26, .93); box-shadow: 0 24px 70px rgba(0,0,0,.2); } +.query-card { padding: 24px; } +.query-card label { display: block; margin-bottom: 13px; color: var(--accent-soft); font: 700 12px ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; } +textarea { width: 100%; resize: vertical; border: 0; outline: 0; color: var(--ink); background: transparent; font: 500 clamp(18px, 2.4vw, 27px)/1.42 Georgia, serif; } +.query-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--line); } +#counter { color: var(--muted); font: 12px ui-monospace, monospace; } +button { appearance: none; border: 0; padding: 14px 20px; color: #141411; background: var(--accent); font-weight: 800; cursor: pointer; transition: transform .16s ease, filter .16s ease; } +button:hover { transform: translateY(-2px); filter: brightness(1.08); } +button:disabled { cursor: wait; filter: grayscale(.6); transform: none; } +.status { margin: 18px 0; padding: 14px 18px; border: 1px solid var(--line); color: var(--muted); background: var(--paper); } +.status.error { border-color: var(--danger); color: var(--danger); } + +.results { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(290px, .85fr); gap: 18px; margin-top: 18px; align-items: start; } +.report-card, .trace-card, .source-card { padding: 26px; } +.side-column { display: grid; gap: 18px; } +.section-heading { display: flex; align-items: baseline; gap: 13px; margin-bottom: 26px; } +.section-heading span { color: var(--accent); font: 700 12px ui-monospace, monospace; } +.section-heading h2 { margin: 0; font: 700 30px/1 Georgia, serif; } +.section-heading.compact h2 { font-size: 22px; } +.answer { color: #ded9cc; font: 16px/1.75 ui-sans-serif, system-ui, sans-serif; } +.answer h2 { margin: 28px 0 12px; color: var(--ink); font: 700 24px Georgia, serif; } +.answer blockquote { margin: 18px 0; padding: 12px 16px; border-left: 3px solid var(--accent); color: var(--accent-soft); background: #181815; } +.answer code { padding: 2px 5px; color: var(--mint); background: #10100f; } +.answer li { margin: 8px 0; } +.trace-list { margin: 0; padding: 0; list-style: none; counter-reset: trace; } +.trace-list li { position: relative; margin-left: 8px; padding: 0 0 22px 24px; border-left: 1px solid var(--line); color: var(--muted); font-size: 13px; } +.trace-list li:last-child { padding-bottom: 0; } +.trace-list li::before { content: ""; position: absolute; left: -5px; top: 3px; width: 9px; height: 9px; border-radius: 50%; background: var(--mint); } +.trace-list strong { display: block; margin-bottom: 5px; color: var(--ink); font-family: ui-monospace, monospace; } +.source-list { display: grid; gap: 11px; } +.source-item { padding: 13px 0; border-top: 1px solid var(--line); } +.source-item:first-child { border-top: 0; padding-top: 0; } +.source-item a { color: var(--ink); font-weight: 750; text-decoration-color: var(--accent); text-underline-offset: 4px; } +.source-item p { margin: 7px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; } +.source-id { color: var(--accent); font: 700 11px ui-monospace, monospace; } +footer { display: flex; justify-content: space-between; gap: 18px; margin-top: 34px; padding-top: 18px; border-top: 1px solid var(--line); color: #77746b; font: 11px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .08em; } + +@media (max-width: 780px) { + .shell { width: min(100% - 24px, 680px); padding-top: 34px; } + .hero { display: block; } + .mode-badge { display: inline-block; margin-top: 26px; } + .results { grid-template-columns: 1fr; } + .notice, footer { align-items: flex-start; flex-direction: column; } + .query-card, .report-card, .trace-card, .source-card { padding: 20px; } +} diff --git a/issue2/demo/tests/test_evidence_board.py b/issue2/demo/tests/test_evidence_board.py new file mode 100644 index 00000000..a281e214 --- /dev/null +++ b/issue2/demo/tests/test_evidence_board.py @@ -0,0 +1,86 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from evidence_board import ( + DemoProvider, + KnowledgeBase, + OpenAICompatibleProvider, + ResearchAgent, + ValidationError, + normalize_question, +) + + +DEMO_ROOT = Path(__file__).resolve().parents[1] + + +class NormalizeQuestionTests(unittest.TestCase): + def test_collapses_whitespace(self): + self.assertEqual(normalize_question(" Hy3 agent\n test "), "Hy3 agent test") + + def test_rejects_short_question(self): + with self.assertRaises(ValidationError): + normalize_question("too short") + + def test_rejects_long_question(self): + with self.assertRaises(ValidationError): + normalize_question("x" * 501) + + +class KnowledgeBaseTests(unittest.TestCase): + def setUp(self): + self.knowledge = KnowledgeBase.from_directory(DEMO_ROOT / "knowledge") + + def test_search_returns_relevant_source(self): + results = self.knowledge.search("Hy3 295B 激活参数") + self.assertTrue(results) + self.assertEqual(results[0]["id"], "model") + + def test_search_order_is_stable(self): + first = [item["id"] for item in self.knowledge.search("Hy3 tool calling 模型")] + second = [item["id"] for item in self.knowledge.search("Hy3 tool calling 模型")] + self.assertEqual(first, second) + + def test_empty_directory_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises(ValueError): + KnowledgeBase.from_directory(Path(directory)) + + +class ProviderPayloadTests(unittest.TestCase): + def test_openrouter_reasoning_shape(self): + provider = OpenAICompatibleProvider( + base_url="https://openrouter.ai/api/v1", + api_key="test", + model="tencent/hy3", + provider_kind="openrouter", + ) + payload = provider.build_payload([{"role": "user", "content": "hello"}], []) + self.assertEqual(payload["reasoning"], {"effort": "low"}) + self.assertNotIn("chat_template_kwargs", payload) + + def test_selfhost_reasoning_shape(self): + provider = OpenAICompatibleProvider( + base_url="http://127.0.0.1:8000/v1/", + api_key="EMPTY", + model="hy3", + ) + payload = provider.build_payload([{"role": "user", "content": "hello"}], []) + self.assertEqual(payload["chat_template_kwargs"], {"reasoning_effort": "low"}) + self.assertEqual(provider.base_url, "http://127.0.0.1:8000/v1") + + +class AgentTests(unittest.TestCase): + def test_demo_runs_tool_then_returns_grounded_output(self): + agent = ResearchAgent(DemoProvider(), KnowledgeBase.from_directory(DEMO_ROOT / "knowledge")) + result = agent.run("请解释 Hy3 模型规模、上下文和工具调用部署要求。") + self.assertEqual(result["mode"], "demo") + self.assertGreaterEqual(len(result["trace"]), 1) + self.assertGreaterEqual(len(result["evidence"]), 1) + self.assertIn("未调用 Hy3", result["answer"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/issue2/demo/tests/test_server.py b/issue2/demo/tests/test_server.py new file mode 100644 index 00000000..f1280af5 --- /dev/null +++ b/issue2/demo/tests/test_server.py @@ -0,0 +1,78 @@ +import json +import threading +import unittest +import urllib.error +import urllib.request +from pathlib import Path + +from evidence_board import DemoProvider, KnowledgeBase, ResearchAgent +from server import make_server + + +DEMO_ROOT = Path(__file__).resolve().parents[1] + + +class ServerTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + agent = ResearchAgent(DemoProvider(), KnowledgeBase.from_directory(DEMO_ROOT / "knowledge")) + cls.server = make_server("127.0.0.1", 0, agent) + cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) + cls.thread.start() + cls.base_url = f"http://127.0.0.1:{cls.server.server_address[1]}" + + @classmethod + def tearDownClass(cls): + cls.server.shutdown() + cls.server.server_close() + cls.thread.join(timeout=2) + + def test_health_reports_demo_mode(self): + with urllib.request.urlopen(f"{self.base_url}/api/health") as response: + payload = json.load(response) + self.assertTrue(payload["ok"]) + self.assertEqual(payload["mode"], "demo") + + def test_research_endpoint(self): + request = urllib.request.Request( + f"{self.base_url}/api/research", + data=json.dumps({"question": "请解释 Hy3 的 Agent 工具调用和上下文能力。"}).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request) as response: + payload = json.load(response) + self.assertTrue(payload["ok"]) + self.assertTrue(payload["trace"]) + + def test_non_json_is_rejected_with_415(self): + request = urllib.request.Request( + f"{self.base_url}/api/research", + data=b"question=not-json", + headers={"Content-Type": "text/plain"}, + method="POST", + ) + with self.assertRaises(urllib.error.HTTPError) as caught: + urllib.request.urlopen(request) + try: + self.assertEqual(caught.exception.code, 415) + finally: + caught.exception.close() + + def test_short_question_is_rejected_with_400(self): + request = urllib.request.Request( + f"{self.base_url}/api/research", + data=json.dumps({"question": "short"}).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with self.assertRaises(urllib.error.HTTPError) as caught: + urllib.request.urlopen(request) + try: + self.assertEqual(caught.exception.code, 400) + finally: + caught.exception.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/issue2/integrations/README.en.md b/issue2/integrations/README.en.md new file mode 100644 index 00000000..85bdeba5 --- /dev/null +++ b/issue2/integrations/README.en.md @@ -0,0 +1,41 @@ +# Use Hy3 in Popular AI Tools + +> 中文:[README.md](README.md) + +These guides use Hy3 through an OpenAI-compatible endpoint and cover an API gateway, terminal coding, and VS Code agents. Every guide includes prerequisites, complete configuration, a first conversation, an end-to-end task, and troubleshooting. + +| Tool | Surface | Guide | End-to-end task | +|:---|:---|:---|:---| +| OpenRouter | Web/API gateway | [Open](openrouter/openrouter.en.md) | Search sources with a tool call and write an evidence report | +| Aider | Terminal coding agent | [Open](aider/aider.en.md) | Create and test a Python slug utility | +| Cline | VS Code agent | [Open](cline/cline.en.md) | Fix a todo app and run its tests | +| Continue | VS Code/CLI agent | [Open](continue/continue.en.md) | Add validation and tests in Agent mode | +| Roo Code | VS Code agent | [Open](roo-code/roo-code.en.md) | Refactor across files and verify regression tests | + +## Endpoint presets + +| Setting | OpenRouter | Self-hosted vLLM/SGLang | +|:---|:---|:---| +| Base URL | `https://openrouter.ai/api/v1` | `http://127.0.0.1:8000/v1` | +| Model | `tencent/hy3` | `hy3` (or your served model name) | +| API key | `OPENROUTER_API_KEY` | Often `EMPTY`; follow your gateway policy | +| Protocol | OpenAI Chat Completions | OpenAI Chat Completions | + +## Shared acceptance task + +The guides use the same verification commands so results can be compared: + +```bash +cd issue2/demo +python3 -m unittest discover -s tests -v +python3 server.py --check +``` + +The resulting offline UI is captured in [Evidence Board offline mode](../assets/evidence-board-offline.png). It proves only the local app and offline path; record a separate live tool interaction after adding your own Hy3 endpoint. + +## Security conventions + +- Never put keys in Markdown, screenshots, tracked files, or browser `localStorage`. +- Prefer environment variables or the product's secret store. +- Enable Hy3's tool-call parser when serving the model if you use Agent mode. +- Start coding agents in review/confirm mode and authorize commands and writes deliberately. diff --git a/issue2/integrations/README.md b/issue2/integrations/README.md new file mode 100644 index 00000000..d5caf5c5 --- /dev/null +++ b/issue2/integrations/README.md @@ -0,0 +1,41 @@ +# 在主流 AI 工具中使用 Hy3 + +> English: [README.en.md](README.en.md) + +这组指南统一使用 Hy3 的 OpenAI 兼容接口,覆盖网页/API、终端和 VS Code Agent。每篇都包含安装要求、完整配置、第一次对话、一个端到端真实任务和排错说明。 + +| 工具 | 形态 | 指南 | 真实任务 | +|:---|:---|:---|:---| +| OpenRouter | 聚合网页/API | [打开](openrouter/openrouter.md) | 用工具调用检索资料并生成证据报告 | +| Aider | 终端编码 Agent | [打开](aider/aider.md) | 创建并测试一个 Python slug 工具 | +| Cline | VS Code Agent | [打开](cline/cline.md) | 修复待办应用并运行测试 | +| Continue | VS Code/CLI Agent | [打开](continue/continue.md) | 用 Agent 模式补输入校验和测试 | +| Roo Code | VS Code Agent | [打开](roo-code/roo-code.md) | 跨文件重构并验证回归测试 | + +## 两种端点 + +| 配置 | OpenRouter | 自建 vLLM/SGLang | +|:---|:---|:---| +| Base URL | `https://openrouter.ai/api/v1` | `http://127.0.0.1:8000/v1` | +| Model | `tencent/hy3` | `hy3`(以部署时的 served name 为准) | +| API Key | `OPENROUTER_API_KEY` | 常用 `EMPTY`,以网关要求为准 | +| 协议 | OpenAI Chat Completions | OpenAI Chat Completions | + +## 统一验收任务 + +为了让不同工具的结果可比较,指南都要求工具在 `issue2/demo/` 上完成一个可运行改动,并执行: + +```bash +cd issue2/demo +python3 -m unittest discover -s tests -v +python3 server.py --check +``` + +最终页面截图见 [Evidence Board 离线模式](../assets/evidence-board-offline.png)。该截图只证明本地作品与离线路径可运行;使用真实 Hy3 的工具交互录屏需要配置自己的端点后另行采集。 + +## 安全约定 + +- 不把 Key 写进 Markdown、截图、仓库文件或浏览器 `localStorage`。 +- 优先使用环境变量或工具自带的 Secret 管理。 +- 自建服务必须在启动时开启 Hy3 对应的 tool-call parser,Agent 模式才可可靠使用工具。 +- 先以只读或需确认模式运行编码 Agent,再按任务逐项授权命令和文件修改。 diff --git a/issue2/integrations/aider/aider.en.md b/issue2/integrations/aider/aider.en.md new file mode 100644 index 00000000..a50a19aa --- /dev/null +++ b/issue2/integrations/aider/aider.en.md @@ -0,0 +1,75 @@ +# Use Hy3 with Aider + +> 中文:[aider.md](aider.md) · [Back to index](../README.en.md) + +Aider is a terminal pair-programming agent. Its official docs support arbitrary OpenAI-compatible endpoints and require the `openai/` model prefix. + +## Install + +Follow the [official installer](https://aider.chat/docs/install.html): + +```bash +python -m pip install aider-install +aider-install +aider --version +``` + +Use the current stable release. The settings below were checked against Aider's [OpenAI-compatible API](https://aider.chat/docs/llms/openai-compat.html) and [options](https://aider.chat/docs/config/options.html) docs on 2026-07-23. + +## Configure + +Self-hosted: + +```bash +export OPENAI_API_BASE=http://127.0.0.1:8000/v1 +export OPENAI_API_KEY=EMPTY +aider --model openai/hy3 --edit-format diff +``` + +OpenRouter: + +```bash +export OPENROUTER_API_KEY='sk-or-v1-...' +aider --model openrouter/tencent/hy3 --edit-format diff +``` + +Keep only non-secret defaults in `.aider.conf.yml`: + +```yaml +model: openrouter/tencent/hy3 +edit-format: diff +auto-commits: false +show-model-warnings: true +``` + +An unknown-price or metadata warning for a new model is not a failed request; judge the actual response and diff. + +## First conversation + +In a temporary Git repository, ask: + +```text +/ask Inspect this repository read-only and list the files you can see. Do not modify anything. +``` + +Verify Aider shows the intended model and `git status --short` remains empty. + +## End-to-end task + +Ask Aider to implement `slugify(text)` in `src/slugify.py`, add at least six `unittest` edge cases in `tests/test_slugify.py`, avoid all other files, and run `python3 -m unittest discover -s tests -v`. + +Use `/add` to scope files, inspect every diff, and trust the test exit code rather than a success sentence. `/undo` reverses the turn, while `auto-commits: false` prevents an unrequested commit. + +The shared repository task produces this UI: + +![Evidence Board offline screenshot](../../assets/evidence-board-offline.png) + +## Troubleshooting + +| Symptom | Fix | +|:---|:---| +| Unknown model warning | Keep the provider prefix and set `--edit-format diff` | +| Self-hosted connection fails | Include `/v1` in `OPENAI_API_BASE` and check the listener | +| Key appears in shell history | Inject it as an environment secret; never pass it inline | +| Too many files changed | `/add` only the required files and state the path boundary | +| Reasoning ignored | Aider's switch sends an OpenAI-style field; verify whether your Hy3 gateway maps it to `chat_template_kwargs` | diff --git a/issue2/integrations/aider/aider.md b/issue2/integrations/aider/aider.md new file mode 100644 index 00000000..30f340c3 --- /dev/null +++ b/issue2/integrations/aider/aider.md @@ -0,0 +1,82 @@ +# 在 Aider 中使用 Hy3 + +> English: [aider.en.md](aider.en.md) · [返回索引](../README.md) + +Aider 是终端中的结对编程 Agent。其官方文档支持任意 OpenAI 兼容端点,模型名需加 `openai/` 前缀。 + +## 安装与要求 + +按 [Aider 官方安装说明](https://aider.chat/docs/install.html)使用隔离安装器: + +```bash +python -m pip install aider-install +aider-install +aider --version +``` + +建议使用当前稳定版;以下配置于 2026-07-23 对照 [OpenAI compatible APIs](https://aider.chat/docs/llms/openai-compat.html) 和 [options reference](https://aider.chat/docs/config/options.html) 核对。 + +## 配置 + +自建端点: + +```bash +export OPENAI_API_BASE=http://127.0.0.1:8000/v1 +export OPENAI_API_KEY=EMPTY +aider --model openai/hy3 --edit-format diff +``` + +OpenRouter 可以直接使用 Aider 的 OpenRouter provider: + +```bash +export OPENROUTER_API_KEY='sk-or-v1-...' +aider --model openrouter/tencent/hy3 --edit-format diff +``` + +`.aider.conf.yml` 可只保存非敏感项: + +```yaml +model: openrouter/tencent/hy3 +edit-format: diff +auto-commits: false +show-model-warnings: true +``` + +不要在 YAML 中写 Key。Hy3 是新模型,Aider 可能提示未知价格或元数据;这不等于请求失败,应以实际响应与文件 diff 为准。 + +## 第一次对话 + +在一个临时 Git 仓库中运行 Aider,输入: + +```text +/ask 只读检查当前仓库,并告诉我你看到了哪些文件;不要修改任何内容。 +``` + +验收:Aider 显示所选模型,返回仓库概览,`git status --short` 仍为空。 + +## 端到端任务 + +```text +在 src/slugify.py 实现 slugify(text),要求支持 Unicode 输入、连续分隔符归一化和空字符串;在 tests/test_slugify.py 用 unittest 覆盖至少 6 个边界条件。完成后运行 python3 -m unittest discover -s tests -v。不要修改其他文件。 +``` + +操作建议: + +1. 用 `/add src/slugify.py tests/test_slugify.py` 限定上下文。 +2. 审查 Aider 给出的 diff 后再接受。 +3. 以真实测试退出码为准,不以“完成”文本为准。 +4. 用 `/undo` 可撤销本轮修改;本指南关闭 `auto-commits`,避免产生未授权提交。 + +最终可让 Aider对本仓库 `issue2/demo/` 做同类改动,并运行统一验收命令。页面结果示例: + +![Evidence Board 离线运行截图](../../assets/evidence-board-offline.png) + +## 常见问题 + +| 症状 | 处理 | +|:---|:---| +| `Unknown model` 警告 | 保留 `openai/` 或 `openrouter/` 前缀;必要时显式指定 `--edit-format diff` | +| 连接自建端点失败 | `OPENAI_API_BASE` 必须包含 `/v1`;确认服务监听地址 | +| Key 泄露到 shell history | 通过安全的环境注入或 `.env`(加入 `.gitignore`)提供,不写在命令参数中 | +| Aider 修改范围过大 | 只 `/add` 必要文件,并在 prompt 明确禁止修改其他路径 | +| 推理参数被忽略 | Aider 的 `--reasoning-effort` 是 OpenAI 风格字段;自建 Hy3 的 `chat_template_kwargs` 未必能由该开关表达,先验证实际服务日志 | diff --git a/issue2/integrations/cline/cline.en.md b/issue2/integrations/cline/cline.en.md new file mode 100644 index 00000000..9990d5a2 --- /dev/null +++ b/issue2/integrations/cline/cline.en.md @@ -0,0 +1,54 @@ +# Use Hy3 with Cline + +> 中文:[cline.md](cline.md) · [Back to index](../README.en.md) + +Cline is a VS Code coding agent that can edit files, run commands, and use a browser. Its official repository supports OpenRouter and arbitrary OpenAI-compatible APIs. Configure secrets through its provider form rather than editing internal secret files. + +## Install + +1. Use VS Code 1.93+ for Cline's terminal integration. +2. Install **Cline** by publisher `saoudrizwan` from the extension marketplace. +3. Record the installed version and prefer the current stable release. These fields were checked against the [official repository](https://github.com/cline/cline) on 2026-07-23. + +## Configure + +OpenRouter: + +```text +API Provider: OpenRouter +OpenRouter API Key: +Model: tencent/hy3 +``` + +Self-hosted: + +```text +API Provider: OpenAI Compatible +Base URL: http://127.0.0.1:8000/v1 +API Key: EMPTY +Model ID: hy3 +``` + +The OpenAI Compatible provider uses `/chat/completions`. Self-hosted vLLM must enable `--tool-call-parser hy_v3 --enable-auto-tool-choice` for agent tools. + +## First conversation + +Disable auto-approval or restrict it to reads, then ask Cline to list the workspace and read its README without writing, running commands, or using the network. Verify its timeline contains only reads and `git status --short` is unchanged. + +## End-to-end task + +Ask Cline to modify only `issue2/demo/`, normalize whitespace, validate questions at 10–500 characters, add success/short/long unit tests, and run the full suite without committing. Review every requested permission, confirm the path boundary, and rerun the tests yourself. + +![Evidence Board offline screenshot](../../assets/evidence-board-offline.png) + +## Troubleshooting + +| Symptom | Fix | +|:---|:---| +| Hy3 absent from list | Refresh OpenRouter and search `tencent/hy3`, or type `hy3` in compatible mode | +| Tool calls become prose | Check the Hy3 tool parser and provider selection | +| HTTP 400 | Remove optional reasoning settings and test a basic request at temperature 0.9 | +| Unsafe autonomy | Disable Auto Approve or allow only reads and test commands | +| Terminal output missing | Update VS Code/Cline and enable shell integration | + +Before submission, record a live Cline tool timeline with Hy3. The repository screenshot only shows the runnable outcome. diff --git a/issue2/integrations/cline/cline.md b/issue2/integrations/cline/cline.md new file mode 100644 index 00000000..059747f8 --- /dev/null +++ b/issue2/integrations/cline/cline.md @@ -0,0 +1,73 @@ +# 在 Cline 中使用 Hy3 + +> English: [cline.en.md](cline.en.md) · [返回索引](../README.md) + +Cline 是 VS Code 编码 Agent,可读写文件、运行命令并使用浏览器。官方仓库确认支持 OpenRouter 与任意 OpenAI-compatible API;本指南使用扩展内的 Provider 表单,避免直接修改其内部 Secret 文件。 + +## 安装与版本 + +1. 使用 VS Code 1.93+(Cline 的终端集成功能要求该版本或更高)。 +2. 在扩展市场搜索并安装发布者为 `saoudrizwan` 的 **Cline**。 +3. 打开扩展详情页记录实际版本;建议使用当前稳定版。本字段于 2026-07-23 对照 [Cline 官方仓库](https://github.com/cline/cline)核对。 + +## 配置 + +### OpenRouter(推荐) + +在 Cline 设置中选择: + +```text +API Provider: OpenRouter +OpenRouter API Key: <通过 Cline Secret 输入框填写> +Model: tencent/hy3 +``` + +### 自建 Hy3 + +```text +API Provider: OpenAI Compatible +Base URL: http://127.0.0.1:8000/v1 +API Key: EMPTY +Model ID: hy3 +``` + +Cline 的 **OpenAI Compatible** 当前走 `/chat/completions`;不要把仅支持 Responses API 的端点误配到此处。Agent 依赖工具调用,自建 vLLM 应包含 `--tool-call-parser hy_v3 --enable-auto-tool-choice`。 + +## 第一次对话 + +先关闭自动批准或把它限制为只读操作,然后发送: + +```text +只读分析当前工作区:列出顶层目录,读取 README,但不要写文件、运行命令或访问网络。最后说明你实际执行了哪些工具。 +``` + +验收:Cline 的工具时间线只有读取动作;`git status --short` 没有新增修改。 + +## 端到端任务 + +复制以下任务到 Cline: + +```text +仅修改 issue2/demo/。给问题输入增加前后空白归一化和 10~500 字长度校验;为成功、过短、过长三个分支补 unittest。运行 python3 -m unittest discover -s issue2/demo/tests -v。遇到失败先解释原因,再请求执行修复;不要提交 Git。 +``` + +验收顺序: + +1. 审查 Cline 请求的每个写入和命令权限。 +2. 确认 diff 没有越过 `issue2/demo/`。 +3. 在 VS Code 终端亲自复跑测试,看到退出码 0。 +4. 启动 `python3 issue2/demo/server.py` 并打开页面。 + +![Evidence Board 离线运行截图](../../assets/evidence-board-offline.png) + +## 常见问题 + +| 症状 | 处理 | +|:---|:---| +| 模型列表没有 Hy3 | OpenRouter 列表刷新后搜索 `tencent/hy3`;自建模式手工填写 `hy3` | +| 工具调用退化成文字 | 检查服务的 Hy3 tool-call parser;确认 Provider 是 OpenAI Compatible 而非 native OpenAI | +| 400 参数错误 | 先关闭额外思考参数,用 Hy3 推荐的 `temperature=0.9` 验证基础对话 | +| 自动执行风险过高 | 关闭 Auto Approve,或只允许读文件和安全测试命令 | +| 终端结果未被读取 | 升级 VS Code/Cline,并确认 shell integration 已启用 | + +提交前需用真实 Hy3 录制一次 Cline 工具时间线;仓库截图只展示最终可运行作品。 diff --git a/issue2/integrations/continue/continue.en.md b/issue2/integrations/continue/continue.en.md new file mode 100644 index 00000000..f0fbc547 --- /dev/null +++ b/issue2/integrations/continue/continue.en.md @@ -0,0 +1,57 @@ +# Use Hy3 with Continue + +> 中文:[continue.md](continue.md) · [Back to index](../README.en.md) + +Continue ships VS Code/JetBrains extensions and the `cn` CLI. Current releases use `config.yaml`; legacy `config.json` is deprecated. + +## Install + +Install **Continue - open-source AI code agent** by `Continue`, or the official `cn` CLI. Prefer the current stable release. The YAML below was checked against the official [OpenAI provider](https://docs.continue.dev/customize/model-providers/top-level/openai) and [config reference](https://docs.continue.dev/reference) on 2026-07-23. + +## Configure + +Save as `~/.continue/config.yaml` and keep the key in an environment secret: + +```yaml +name: Hy3 Agent +version: 1.0.0 +schema: v1 + +models: + - name: Hy3 via OpenRouter + provider: openai + model: tencent/hy3 + apiBase: https://openrouter.ai/api/v1 + apiKey: ${{ secrets.OPENROUTER_API_KEY }} + capabilities: + - tool_use + roles: + - chat + - edit + - apply + defaultCompletionOptions: + temperature: 0.9 + maxTokens: 4096 +``` + +For self-hosting, use `http://127.0.0.1:8000/v1`, model `hy3`, and the gateway's key policy. Explicit `tool_use` enables Agent tools for a new custom model. + +## First conversation + +Reload the config, select **Hy3 via OpenRouter**, and ask Chat mode to summarize only the open README with the exact file path and no edits. Verify the model ID in logs and an unchanged worktree. + +## End-to-end task + +In Agent mode, ask Continue to modify only `issue2/demo/`, return HTTP 415 for non-JSON research requests, add an HTTP test, run the full suite, and avoid dependencies and commits. Review each Apply diff and rerun tests independently. + +![Evidence Board offline screenshot](../../assets/evidence-board-offline.png) + +## Troubleshooting + +| Symptom | Fix | +|:---|:---| +| YAML ignored | Validate `name/version/schema`, reload, and inspect Continue logs | +| Agent unavailable | Add `capabilities: [tool_use]` and verify endpoint tools support | +| `/responses` fails | Add `useResponsesApi: false` to force Chat Completions | +| Secret empty | Export before launching VS Code and fully restart it | +| Unrelated Apply edits | Constrain paths in the prompt/rules and inspect the diff | diff --git a/issue2/integrations/continue/continue.md b/issue2/integrations/continue/continue.md new file mode 100644 index 00000000..ec9e483d --- /dev/null +++ b/issue2/integrations/continue/continue.md @@ -0,0 +1,69 @@ +# 在 Continue 中使用 Hy3 + +> English: [continue.en.md](continue.en.md) · [返回索引](../README.md) + +Continue 提供 VS Code/JetBrains 扩展和 `cn` CLI。当前配置入口是 `config.yaml`;旧 `config.json` 已弃用。 + +## 安装与版本 + +- 在扩展市场安装发布者为 `Continue` 的 **Continue - open-source AI code agent**,或安装官方 `cn` CLI。 +- 建议使用当前稳定版。以下 YAML 于 2026-07-23 对照 [OpenAI provider 文档](https://docs.continue.dev/customize/model-providers/top-level/openai)、[config.yaml reference](https://docs.continue.dev/reference)核对。 + +## 配置 + +将以下内容保存到 `~/.continue/config.yaml`。推荐把 Key 放入环境 Secret,而不是直接写值: + +```yaml +name: Hy3 Agent +version: 1.0.0 +schema: v1 + +models: + - name: Hy3 via OpenRouter + provider: openai + model: tencent/hy3 + apiBase: https://openrouter.ai/api/v1 + apiKey: ${{ secrets.OPENROUTER_API_KEY }} + capabilities: + - tool_use + roles: + - chat + - edit + - apply + defaultCompletionOptions: + temperature: 0.9 + maxTokens: 4096 +``` + +自建服务只需把 `apiBase` 改成 `http://127.0.0.1:8000/v1`、`model` 改成 `hy3`,并使用网关要求的 Key。Hy3 属于新模型,显式增加 `tool_use` 可让 Continue 开放 Agent 工具能力。 + +## 第一次对话 + +1. 重载 Continue 配置并在模型选择器中选 **Hy3 via OpenRouter**。 +2. 切到 Chat 模式,发送: + + ```text + 只根据当前打开的 README 回答:这个仓库是什么?引用你实际读取的文件路径,不要修改文件。 + ``` + +3. 验收:回答引用正确文件,Continue 日志中的 model 为 `tencent/hy3`,仓库无变化。 + +## 端到端任务(Agent 模式) + +```text +仅修改 issue2/demo/:为 POST /api/research 增加 JSON Content-Type 检查,非 JSON 返回 415;添加覆盖该分支的 HTTP 测试;运行完整 unittest。不要安装依赖,不要提交。 +``` + +显式 `capabilities: [tool_use]` 后才应进入 Agent 模式。接受 Apply 前逐文件查看 diff;结束后在独立终端复跑测试和 `python3 issue2/demo/server.py --check`。 + +![Evidence Board 离线运行截图](../../assets/evidence-board-offline.png) + +## 常见问题 + +| 症状 | 处理 | +|:---|:---| +| YAML 未生效 | 检查顶层 `name/version/schema`,重载配置并查看 Continue 日志 | +| Agent 模式不可选 | 增加 `capabilities: [tool_use]`,并确认端点确实支持 OpenAI tools | +| 调到 `/responses` 失败 | Hy3 配置使用普通模型名;如版本误判,可加 `useResponsesApi: false` 强制 Chat Completions | +| Secret 解析为空 | 从启动 VS Code 的环境导出 `OPENROUTER_API_KEY`,再完全重启 VS Code | +| Apply 破坏无关文件 | 在 prompt 和 Continue rules 中限制路径,应用前审查 diff | diff --git a/issue2/integrations/openrouter/openrouter.en.md b/issue2/integrations/openrouter/openrouter.en.md new file mode 100644 index 00000000..5a525562 --- /dev/null +++ b/issue2/integrations/openrouter/openrouter.en.md @@ -0,0 +1,61 @@ +# Use Hy3 with OpenRouter + +> 中文:[openrouter.md](openrouter.md) · [Back to index](../README.en.md) + +OpenRouter is the simplest path when you do not want to operate an eight-GPU inference service. The fields below were checked against the [official Hy3 model page](https://openrouter.ai/tencent/hy3) on 2026-07-23. + +## Requirements + +- An OpenRouter account and API key +- `curl`, or Python 3.10+ for the showcase +- Model ID `tencent/hy3`; check the model page before relying on the optional `tencent/hy3:free` route + +## Configuration + +```bash +export HY3_PROVIDER=openrouter +export HY3_BASE_URL=https://openrouter.ai/api/v1 +export HY3_MODEL=tencent/hy3 +export HY3_API_KEY='sk-or-v1-...' +``` + +Never commit the key. OpenRouter exposes OpenAI-compatible `POST /chat/completions`; set reasoning depth with `reasoning.effort`. + +## First conversation + +```bash +curl "$HY3_BASE_URL/chat/completions" \ + -H "Authorization: Bearer $HY3_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "tencent/hy3", + "messages": [{"role": "user", "content": "Reply only with READY and your model ID."}], + "temperature": 0.9, + "reasoning": {"effort": "low"} + }' +``` + +Accept only an HTTP 200 with a non-empty `choices[0].message.content`. + +## End-to-end task: Evidence Board + +1. Start the showcase from `issue2/demo` with `python3 server.py`. +2. Open `http://127.0.0.1:8765` and confirm the header says **Live Hy3**. +3. Ask the app to explain why Hy3 fits agent workflows and to cite its size, context length, and serving requirements. +4. Verify the trace contains a `search_knowledge_base` call followed by a final cited report. + +Use `HY3_DEMO_MODE=1 python3 server.py` only for offline UI and retrieval checks. It is visibly marked and is not live-model evidence. + +![Evidence Board offline screenshot](../../assets/evidence-board-offline.png) + +## Troubleshooting + +| Symptom | Check | +|:---|:---| +| `401` | Complete key, whitespace, and account credit | +| `404 model not found` | Use `tencent/hy3`, not the self-hosted name `hy3` | +| No tool call | Include `tools`, confirm route support, and retry a one-tool prompt | +| Slow output | Use low reasoning and a shorter prompt first | +| Unbounded spend | Add a key limit and inspect response `usage` | + +Before submission, record a live clip under one minute showing model ID, prompt, tool trace, and report with the key hidden. This repository does not fabricate that online evidence. diff --git a/issue2/integrations/openrouter/openrouter.md b/issue2/integrations/openrouter/openrouter.md new file mode 100644 index 00000000..61081c20 --- /dev/null +++ b/issue2/integrations/openrouter/openrouter.md @@ -0,0 +1,74 @@ +# 在 OpenRouter 中使用 Hy3 + +> English: [openrouter.en.md](openrouter.en.md) · [返回索引](../README.md) + +OpenRouter 适合不自建 8 卡推理服务、希望直接按量调用 Hy3 的用户。以下字段于 2026-07-23 按 [OpenRouter 的 Hy3 模型页](https://openrouter.ai/tencent/hy3)核对。 + +## 要求 + +- OpenRouter 账号和可用 API Key +- `curl`,或 Python 3.10+(小作品本身仅使用标准库) +- 模型 ID:`tencent/hy3`;如要试用免费路由,可在模型页确认当时是否仍提供 `tencent/hy3:free` + +## 配置 + +```bash +export HY3_PROVIDER=openrouter +export HY3_BASE_URL=https://openrouter.ai/api/v1 +export HY3_MODEL=tencent/hy3 +export HY3_API_KEY='sk-or-v1-...' +``` + +不要把 Key 写入脚本或提交到 Git。OpenRouter 使用 OpenAI 兼容的 `POST /chat/completions`;推理强度可用 `reasoning.effort` 控制。 + +## 第一次对话 + +```bash +curl "$HY3_BASE_URL/chat/completions" \ + -H "Authorization: Bearer $HY3_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "tencent/hy3", + "messages": [{"role": "user", "content": "只回复 READY,并说明你的模型 ID。"}], + "temperature": 0.9, + "reasoning": {"effort": "low"} + }' +``` + +验收:HTTP 200,`choices[0].message.content` 非空;不要仅以控制台出现请求记录判定成功。 + +## 端到端任务:证据研究板 + +1. 启动本仓库的小作品: + + ```bash + cd issue2/demo + python3 server.py + ``` + +2. 打开 `http://127.0.0.1:8765`,确认页头显示“实时 Hy3”。 +3. 输入: + + ```text + 根据内置资料解释 Hy3 为什么适合 Agent,并列出模型规模、上下文和部署时的工具调用要求。每条结论必须带来源。 + ``` + +4. 验收响应中的工具轨迹:第一次模型响应调用 `search_knowledge_base`,服务端回传检索片段,第二次响应给出带来源的报告。 + +本地无 Key 时可用 `HY3_DEMO_MODE=1 python3 server.py` 检查界面和检索链路,但页面会明确标为离线模式,不能把该输出当成 Hy3 运行证据。 + +![Evidence Board 离线运行截图](../../assets/evidence-board-offline.png) + +## 常见问题 + +| 症状 | 检查 | +|:---|:---| +| `401` | Key 是否完整、是否带多余空格、账户额度是否可用 | +| `404 model not found` | 必须使用 `tencent/hy3`,不是自建端点的 `hy3` | +| 无工具调用 | 请求是否包含 `tools`;模型路由是否支持 tool calling;先用低复杂度单工具任务排查 | +| 输出慢 | 深度推理和长输出会增加延迟;先改为 `low` 并缩短问题 | +| 费用不可控 | 在 OpenRouter 设置 Key 限额,并在响应 `usage` 中核对 token | + +## 证据清单 + +提交前请另行录制一次 ≤1 分钟的实时调用:画面应同时包含模型 ID、问题、工具轨迹和最终报告,并遮蔽 Key。本仓库不伪造该在线证据。 diff --git a/issue2/integrations/roo-code/roo-code.en.md b/issue2/integrations/roo-code/roo-code.en.md new file mode 100644 index 00000000..5e8c3ac7 --- /dev/null +++ b/issue2/integrations/roo-code/roo-code.en.md @@ -0,0 +1,46 @@ +# Use Hy3 with Roo Code + +> 中文:[roo-code.md](roo-code.md) · [Back to index](../README.en.md) + +Roo Code is a VS Code coding agent with modes and provider profiles. Its official provider docs list **OpenAI Compatible**, which maps directly to Hy3. + +## Install + +Install **Roo Code** from the VS Code marketplace and verify the publisher against the current official listing. Record the actual extension version and prefer the current stable release. The fields below were checked against [Roo Code provider docs](https://roocodeinc.github.io/Roo-Code/providers/) on 2026-07-23. + +## Provider profile + +Create a `Hy3` profile: + +```text +API Provider: OpenAI Compatible +Base URL: https://openrouter.ai/api/v1 +API Key: +Model ID: tencent/hy3 +Temperature: 0.9 +Context Window: 262144 +``` + +For self-hosting use `http://127.0.0.1:8000/v1`, key `EMPTY`, and model `hy3`. Keep Native Tools enabled when shown, and enable Hy3's server-side tool parser. The actual context limit remains provider-dependent. + +## First conversation + +In **Ask** mode with write/command auto-approval disabled, request three verifiable Hy3 facts from `README_CN.md`, each with a path and section. Confirm only reads occurred and the worktree is unchanged. + +## End-to-end task + +In **Code** mode, ask Roo to modify only `issue2/demo/`, replace substring retrieval with testable token scoring, cover stable Chinese and English ordering with tests first, run the suite, and avoid network, dependencies, and commits. Approve only scoped reads, individual writes, and the exact test command. + +![Evidence Board offline screenshot](../../assets/evidence-board-offline.png) + +## Troubleshooting + +| Symptom | Fix | +|:---|:---| +| Profile switches back | Reselect `Hy3` in the task header and verify in a new task | +| Model not discovered | Enter it manually; OpenRouter requires `tencent/hy3` | +| Tool JSON errors | Enable Native Tools, verify server parser, and reduce parallel tools | +| Context overflow | Stay below provider limits and remove unrelated files/logs | +| Excessive auto-approval | Save least-privilege mode/profile settings and confirm sensitive commands | + +Record a separate live Hy3 tool timeline before submission. The local screenshot documents only the runnable outcome. diff --git a/issue2/integrations/roo-code/roo-code.md b/issue2/integrations/roo-code/roo-code.md new file mode 100644 index 00000000..159b600c --- /dev/null +++ b/issue2/integrations/roo-code/roo-code.md @@ -0,0 +1,68 @@ +# 在 Roo Code 中使用 Hy3 + +> English: [roo-code.en.md](roo-code.en.md) · [返回索引](../README.md) + +Roo Code 是 VS Code 中支持多模式和配置 Profile 的编码 Agent。官方 Provider 文档列出 **OpenAI Compatible**,适合直接接入 Hy3。 + +## 安装与版本 + +1. 在 VS Code 扩展市场安装 **Roo Code**,核对发布者为 `RooVeterinaryInc`(以官方市场页当前信息为准)。 +2. 记录扩展详情页中的实际版本,建议使用当前稳定版。 +3. 以下字段于 2026-07-23 对照 [Roo Code Provider 文档](https://roocodeinc.github.io/Roo-Code/providers/)核对。 + +## 配置 Profile + +打开 Roo Code → Settings → Provider Profiles,新建 `Hy3`: + +```text +API Provider: OpenAI Compatible +Base URL: https://openrouter.ai/api/v1 +API Key: <在 Secret 输入框填写> +Model ID: tencent/hy3 +Temperature: 0.9 +Context Window: 262144 +``` + +自建端点使用: + +```text +Base URL: http://127.0.0.1:8000/v1 +API Key: EMPTY +Model ID: hy3 +``` + +如果当前版本展示 Native Tools 开关,保持开启;自建端同时启用 Hy3 tool-call parser。上下文值按 OpenRouter 模型页的 262K 展示填写,最终可用长度仍受 Provider 限制。 + +## 第一次对话 + +选择 **Ask** 模式,关闭所有写入/命令的自动批准,发送: + +```text +读取 README_CN.md,给出三条可以直接从文件核对的 Hy3 信息,每条附文件路径和小节名。不要修改文件或执行命令。 +``` + +验收:Roo 只读取文件,答案可逐条在 README 中找到,工作树未变化。 + +## 端到端任务 + +切到 **Code** 模式: + +```text +只修改 issue2/demo/。把知识库检索从简单子串匹配重构为可测试的分词计分;中文和英文问题都必须返回稳定排序。先补失败测试,再实现,再运行全部 unittest。不要联网、安装依赖或提交 Git。 +``` + +建议授权策略:允许读 `issue2/demo/`;写入逐次确认;仅批准明确的 `python3 -m unittest ...`。完成后检查工具时间线确实经历“失败测试 → 实现 → 通过测试”,而非只展示最终文本。 + +![Evidence Board 离线运行截图](../../assets/evidence-board-offline.png) + +## 常见问题 + +| 症状 | 处理 | +|:---|:---| +| Profile 保存后回到其他模型 | 在当前任务顶部重新选择 `Hy3` Profile,新建任务验证 | +| Model ID 不被自动发现 | OpenAI Compatible 允许手工输入;OpenRouter 必须是 `tencent/hy3` | +| 工具调用 JSON 出错 | 开启 Native Tools;检查 vLLM/SGLang 的 Hy3 parser;先减少并行工具数 | +| 上下文溢出 | Context Window 不要超过 Provider 模型页;减少无关文件和终端日志 | +| 自动批准范围过宽 | 用 Profile/模式分别保存最小权限,敏感命令始终手工确认 | + +提交前应再录制一次真实 Hy3 的 Roo 工具时间线;本地截图只作为作品运行结果。