Skip to content

feat(sdk): 实现 Python SDK 最小可用(Client + Adapter + GenericAdapter + Lea…#17

Merged
chenchenchenchencj merged 1 commit into
masterfrom
cccc
Mar 7, 2026
Merged

feat(sdk): 实现 Python SDK 最小可用(Client + Adapter + GenericAdapter + Lea…#17
chenchenchenchencj merged 1 commit into
masterfrom
cccc

Conversation

@chenchenchenchencj

Copy link
Copy Markdown
Collaborator

…rningEngine)

Made-with: Cursor

Copilot AI review requested due to automatic review settings March 7, 2026 08:31
@chenchenchenchencj chenchenchenchencj merged commit 2c7b0e3 into master Mar 7, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a minimal viable GeneHub Python SDK under packages/sdk/python, aligning core capabilities with the existing TypeScript SDK so Python-side agents can call the Registry API, install genes via an adapter, and run the learning-task workflow.

Changes:

  • Introduces GeneHubClient (Registry HTTP client), adapter abstractions (GeneAdapter) and a filesystem-based GenericAdapter.
  • Adds a minimal LearningEngine for creating learning tasks and parsing learning results.
  • Adds Python packaging (pyproject/lockfile), unit tests, and updates repo docs to reflect Python SDK availability.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/sdk/python/uv.lock Adds a uv lockfile for the Python SDK dependencies.
packages/sdk/python/pyproject.toml Defines Python package metadata, dependencies, Ruff + pytest config.
packages/sdk/python/README.md Documents Python SDK purpose, structure, and usage examples.
packages/sdk/python/src/genehub_sdk/init.py Exposes the public Python SDK surface (client/adapters/learning/types).
packages/sdk/python/src/genehub_sdk/types.py Adds TypedDict-based models for manifests, API responses, adapter I/O.
packages/sdk/python/src/genehub_sdk/client.py Implements GeneHubClient + GeneHubError for Registry API calls.
packages/sdk/python/src/genehub_sdk/adapters/init.py Exports adapter types and the generic adapter.
packages/sdk/python/src/genehub_sdk/adapters/base.py Defines the GeneAdapter abstract interface.
packages/sdk/python/src/genehub_sdk/adapters/generic.py Implements a filesystem-based GenericAdapter.
packages/sdk/python/src/genehub_sdk/learning/init.py Exports LearningEngine.
packages/sdk/python/src/genehub_sdk/learning/engine.py Minimal learning-task creation + result parsing implementation.
packages/sdk/python/tests/test_client.py Tests GeneHubClient using pytest-httpx mocking.
packages/sdk/python/tests/test_generic_adapter.py Tests GenericAdapter install/uninstall/list/version behavior.
packages/sdk/python/tests/test_learning_engine.py Tests learning task creation + result parsing behavior.
docs/architecture.md Updates architecture docs to mark Python SDK as minimally implemented and lists its tree.
README.md Updates root README to list the Python SDK package/path.
.gitignore Adds common Python virtualenv/cache ignores.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

from genehub_sdk import GeneHubClient

client = GeneHubClient(base_url="https://registry.genehub.dev", token="ghb_xxx")
genes = client.search_genes(q="code")

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README 示例里调用了 client.search_genes(q="code"),但 GeneHubClient.search_genes 的参数名是 query(或直接用第一个位置参数)。按当前示例会触发 TypeError: unexpected keyword argument 'q';建议将示例改为 client.search_genes("code")client.search_genes(query="code")

Suggested change
genes = client.search_genes(q="code")
genes = client.search_genes(query="code")

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +78
qs = "&".join(f"{k}={v}" for k, v in params)
path = f"/api/v1/genes?{qs}" if qs else "/api/v1/genes"
result = self._request("GET", path)

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

search_genes 这里手动用 "&".join(f"{k}={v}") 拼查询字符串,没有做 URL 编码;当 query/tags/category 等包含空格、&?、非 ASCII 等字符时会生成非法或语义错误的 URL。建议改为使用 httpxparams=(或 urllib.parse.urlencode)让库负责编码。

Suggested change
qs = "&".join(f"{k}={v}" for k, v in params)
path = f"/api/v1/genes?{qs}" if qs else "/api/v1/genes"
result = self._request("GET", path)
result = self._request("GET", "/api/v1/genes", params=params)

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +46
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
url = f"{self._base_url}{path}"
with httpx.Client(timeout=30.0) as client:
resp = client.request(
method,
url,
headers=self._headers(),
**kwargs,
)
data = resp.json()
body = data if isinstance(data, dict) else {}
code = body.get("code", -1)
if not resp.is_success or code != 0:
msg = body.get("message") or f"HTTP {resp.status_code}"
err_code = body.get("error_code")
raise GeneHubError(msg, error_code=err_code)

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_request 里无论 HTTP 状态码/响应内容是什么都会直接 resp.json();当 Registry 返回非 JSON(例如 502 HTML、空响应等)时会抛出解码异常并掩盖原始错误信息。建议捕获 JSON 解码异常,在失败时至少用 resp.text/状态码组装错误,并确保仍抛出 GeneHubError

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +71
class GeneManifest(TypedDict, total=False):
slug: str
name: str
version: str
description: str
short_description: str
category: str
tags: list[str]
icon: str | None
author: Author | None
compatibility: list[CompatibilityEntry]
dependencies: list[DependencyEntry]
synergies: list[str]
skill: Skill
rules: list[Rule]
config: GeneConfig | None
learning: Learning | None

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeneManifest 定义为 TypedDict(total=False) 会让 slug/name/version/... 等在类型层面都变成可选,但实现里(例如 GenericAdapter.install)又直接用 manifest["slug"]/manifest["version"] 作为必填字段。建议把必填字段改为 Required(或将 TypedDict 设为 total=True 并对可选字段用 NotRequired/Optional),以免类型提示与实际约束不一致。

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +58
"created_at": __import__("datetime")
.datetime.now(__import__("datetime").timezone.utc)
.isoformat(),

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里通过 __import__("datetime") 生成 created_at 可读性较差,也会让静态分析/重构工具更难处理。建议在模块顶部直接 from datetime import datetime, timezone(或 UTC)并使用常规调用来生成 ISO 时间戳。

Copilot uses AI. Check for mistakes.

gene_yaml = target_dir / "gene.yaml"
gene_yaml.write_text(
yaml.dump(manifest, allow_unicode=True, default_flow_style=False), encoding="utf-8"

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 yaml.dump(...) 写 manifest,若传入的数据里混入非基础类型(自定义对象等)可能被序列化成 !!python/object 之类的 Python 专有标签,后续再用 safe_load 读取会失败并导致 list()/get_installed_version 跳过该基因。建议使用 yaml.safe_dump(并确保 manifest 仅包含 JSON/YAML 基础类型)来避免生成不可移植的 YAML。

Suggested change
yaml.dump(manifest, allow_unicode=True, default_flow_style=False), encoding="utf-8"
yaml.safe_dump(manifest, allow_unicode=True, default_flow_style=False),
encoding="utf-8",

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants