Python SDK: write DSH plugins across languages
When Python SDK beats TS, package layout, the @tool decorator, Python-type-to-JSON-Schema mapping, asyncio traps, and the pydantic v2-only limitation.
9 min read
What you'll learn
- Call DSH Cordis services from a Python process (not limited to TS)
- Solve type declaration and async traps when Python ↔ TypeScript interoperate
- Write a Python-only tool plugin
Version
Compatible with: dsh 0.1.1-rc.2 (compiled from official packages/host-python/ + docs/user/develop/sdk/)
When to use Python SDK instead of writing TS
DSH's Cordis core is TS, but the host exposes Python bindings (@deepseek-ai/dsh-host-python). Common scenarios:
- Team's primary language is Python, tooling is all Python (data science / ML / internal platform)
- Want to plug into existing Python assets (pandas pipelines / internal portal Python SDKs)
- Don't want to maintain TS code but still want to add capabilities to DSH
When NOT to use it:
- Hot-path tools (Python IPC adds 5-15ms overhead, plain TS is faster)
- Tools with complex type contracts (TS type system + IDE autocomplete >> Python)
Python package layout
my-python-plugin/
├── pyproject.toml
├── dsh.bundle
└── src/
└── my_plugin/
├── __init__.py
└── tools.py
Key pyproject.toml fields:
[project]
name = "my-dsh-plugin"
version = "0.1.0"
dependencies = [
"deepseek-harness-host-python>=0.1.1",
"pandas>=2.0",
]
dsh.bundle is identical to TS plugins:
name: my-dsh-plugin
version: 0.1.0
entry: src.my_plugin.tools:apply
language: python
Write a minimal Python tool
from deepseek_harness import Context, tool
def apply(ctx: Context):
@tool(name="load_csv", description="Load a CSV file into a pandas DataFrame")
def load_csv(path: str) -> dict:
import pandas as pd
df = pd.read_csv(path)
return {
"rows": len(df),
"columns": list(df.columns),
"head": df.head(5).to_dict(orient="records"),
}
The @tool decorator registers the function as a model-callable tool; type annotations are converted to JSON Schema for the model.
Type declarations vs TS interop
Python types → JSON Schema (model-visible)
str→{"type": "string"}int/float→{"type": "integer" | "number"}bool→{"type": "boolean"}list[X]→{"type": "array", "items": <X schema>}dict[str, X]→{"type": "object", "additionalProperties": <X schema>}Optional[X]→[X schema, {"type": "null"}]
Complex types (pydantic BaseModel / dataclass) auto-expand; don't use Any.
TS calling Python plugins
It works the other way too — a TS host can @tool-call a Python plugin's tools, routing calls via IPC to the Python process at runtime.
Async traps
DSH tool calls are async. The Python SDK uses asyncio + aiohttp:
import aiohttp
@tool(name="fetch_url", description="Fetch a URL and return text content")
async def fetch_url(url: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return {"status": resp.status, "text": await resp.text()}
Don't put blocking sync calls (requests.get / time.sleep / large pd.read_csv) inside @tool functions — they freeze the entire agent loop. CPU-bound work should be wrapped with asyncio.to_thread().
Troubleshooting
- "Python plugin not loading": check
dsh.bundlehaslanguage: python+entryis importable - "type errors silently swallowed": when tool raises, SDK defaults to
ToolErrorbut model sees no stack trace; explicitlyraise ToolError("...", retryable=False)for custom errors - "frequent IPC lag": batch many small calls into one big call (batch fetch / batch read), or move back to TS
FAQ
Can a Python plugin call a TS plugin's tool?
Yes but not recommended — cross-language IPC is hard to control and debug. Have Python plugins expose only their own tools; let TS orchestrate.
Are both pydantic v1 and v2 supported?
Only pydantic v2 (better performance + richer type description).