Skip to main content

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.bundle has language: python + entry is importable
  • "type errors silently swallowed": when tool raises, SDK defaults to ToolError but model sees no stack trace; explicitly raise 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).