dhara-extension 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dhara_extension-0.1.0/PKG-INFO +135 -0
- dhara_extension-0.1.0/README.md +117 -0
- dhara_extension-0.1.0/pyproject.toml +28 -0
- dhara_extension-0.1.0/setup.cfg +4 -0
- dhara_extension-0.1.0/src/dhara_extension/__init__.py +34 -0
- dhara_extension-0.1.0/src/dhara_extension/extension.py +186 -0
- dhara_extension-0.1.0/src/dhara_extension/protocol.py +160 -0
- dhara_extension-0.1.0/src/dhara_extension.egg-info/PKG-INFO +135 -0
- dhara_extension-0.1.0/src/dhara_extension.egg-info/SOURCES.txt +10 -0
- dhara_extension-0.1.0/src/dhara_extension.egg-info/dependency_links.txt +1 -0
- dhara_extension-0.1.0/src/dhara_extension.egg-info/top_level.txt +1 -0
- dhara_extension-0.1.0/tests/test_extension.py +127 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhara-extension
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for building Dhara extensions
|
|
5
|
+
Author-email: Zosma AI <dev@zosma.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: dhara,extension,json-rpc,ai,coding-agent
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# dhara-extension-py
|
|
20
|
+
|
|
21
|
+
Python SDK for building [Dhara](https://github.com/zosmaai/dhara) extensions.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install dhara-extension
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install from source:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd packages/dhara-extension-py
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
Create a file `hello-ext/main.py`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
#!/usr/bin/env python3
|
|
42
|
+
"""A simple Dhara extension using the Python SDK."""
|
|
43
|
+
|
|
44
|
+
from dhara_extension import Extension
|
|
45
|
+
|
|
46
|
+
ext = Extension(
|
|
47
|
+
name="hello-ext",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description="A friendly hello extension",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@ext.tool(
|
|
53
|
+
name="hello",
|
|
54
|
+
description="Greet someone by name",
|
|
55
|
+
parameters={
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"name": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Name to greet",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"required": ["name"],
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
def hello(input_data):
|
|
67
|
+
name = input_data.get("name", "World")
|
|
68
|
+
greeting = f"Hello, {name}! Welcome to Dhara."
|
|
69
|
+
return {
|
|
70
|
+
"content": [
|
|
71
|
+
{"type": "text", "text": greeting},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ext.tool(
|
|
76
|
+
name="count",
|
|
77
|
+
description="Count items in a list",
|
|
78
|
+
)
|
|
79
|
+
def count(input_data):
|
|
80
|
+
items = input_data.get("items", [])
|
|
81
|
+
return {
|
|
82
|
+
"content": [
|
|
83
|
+
{"type": "text", "text": f"Counted {len(items)} items."},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
ext.run()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
And a `manifest.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"name": "hello-ext",
|
|
96
|
+
"version": "1.0.0",
|
|
97
|
+
"runtime": {
|
|
98
|
+
"type": "subprocess",
|
|
99
|
+
"command": "python3 main.py",
|
|
100
|
+
"protocol": "json-rpc"
|
|
101
|
+
},
|
|
102
|
+
"provides": {
|
|
103
|
+
"tools": ["hello", "count"]
|
|
104
|
+
},
|
|
105
|
+
"capabilities": []
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `Extension(name, version, description="", debug=False)`
|
|
112
|
+
|
|
113
|
+
Main extension class.
|
|
114
|
+
|
|
115
|
+
- **`tool(name, description, parameters, capabilities)`** — Decorator to register a tool handler
|
|
116
|
+
- **`run()`** — Start the JSON-RPC stdin/stdout loop
|
|
117
|
+
|
|
118
|
+
### `create_extension(name, version, description="")`
|
|
119
|
+
|
|
120
|
+
Convenience function to create an `Extension` instance.
|
|
121
|
+
|
|
122
|
+
## Protocol
|
|
123
|
+
|
|
124
|
+
Extensions communicate with Dhara via JSON-RPC 2.0 over stdin/stdout:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
|
|
128
|
+
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"0.1.0","name":"hello-ext","tools":[...]}}
|
|
129
|
+
|
|
130
|
+
→ {"jsonrpc":"2.0","id":2,"method":"tools/execute",
|
|
131
|
+
"params":{"toolName":"hello","input":{"name":"World"}}}
|
|
132
|
+
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hello, World!"}]}}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
See the [Dhara Extension Protocol](https://github.com/zosmaai/dhara/blob/main/spec/extension-protocol.md) for details.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# dhara-extension-py
|
|
2
|
+
|
|
3
|
+
Python SDK for building [Dhara](https://github.com/zosmaai/dhara) extensions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install dhara-extension
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd packages/dhara-extension-py
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
Create a file `hello-ext/main.py`:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
#!/usr/bin/env python3
|
|
24
|
+
"""A simple Dhara extension using the Python SDK."""
|
|
25
|
+
|
|
26
|
+
from dhara_extension import Extension
|
|
27
|
+
|
|
28
|
+
ext = Extension(
|
|
29
|
+
name="hello-ext",
|
|
30
|
+
version="1.0.0",
|
|
31
|
+
description="A friendly hello extension",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@ext.tool(
|
|
35
|
+
name="hello",
|
|
36
|
+
description="Greet someone by name",
|
|
37
|
+
parameters={
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"name": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Name to greet",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"required": ["name"],
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
def hello(input_data):
|
|
49
|
+
name = input_data.get("name", "World")
|
|
50
|
+
greeting = f"Hello, {name}! Welcome to Dhara."
|
|
51
|
+
return {
|
|
52
|
+
"content": [
|
|
53
|
+
{"type": "text", "text": greeting},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@ext.tool(
|
|
58
|
+
name="count",
|
|
59
|
+
description="Count items in a list",
|
|
60
|
+
)
|
|
61
|
+
def count(input_data):
|
|
62
|
+
items = input_data.get("items", [])
|
|
63
|
+
return {
|
|
64
|
+
"content": [
|
|
65
|
+
{"type": "text", "text": f"Counted {len(items)} items."},
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
ext.run()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
And a `manifest.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"name": "hello-ext",
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
"runtime": {
|
|
80
|
+
"type": "subprocess",
|
|
81
|
+
"command": "python3 main.py",
|
|
82
|
+
"protocol": "json-rpc"
|
|
83
|
+
},
|
|
84
|
+
"provides": {
|
|
85
|
+
"tools": ["hello", "count"]
|
|
86
|
+
},
|
|
87
|
+
"capabilities": []
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `Extension(name, version, description="", debug=False)`
|
|
94
|
+
|
|
95
|
+
Main extension class.
|
|
96
|
+
|
|
97
|
+
- **`tool(name, description, parameters, capabilities)`** — Decorator to register a tool handler
|
|
98
|
+
- **`run()`** — Start the JSON-RPC stdin/stdout loop
|
|
99
|
+
|
|
100
|
+
### `create_extension(name, version, description="")`
|
|
101
|
+
|
|
102
|
+
Convenience function to create an `Extension` instance.
|
|
103
|
+
|
|
104
|
+
## Protocol
|
|
105
|
+
|
|
106
|
+
Extensions communicate with Dhara via JSON-RPC 2.0 over stdin/stdout:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
|
|
110
|
+
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"0.1.0","name":"hello-ext","tools":[...]}}
|
|
111
|
+
|
|
112
|
+
→ {"jsonrpc":"2.0","id":2,"method":"tools/execute",
|
|
113
|
+
"params":{"toolName":"hello","input":{"name":"World"}}}
|
|
114
|
+
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hello, World!"}]}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See the [Dhara Extension Protocol](https://github.com/zosmaai/dhara/blob/main/spec/extension-protocol.md) for details.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta:__legacy__"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dhara-extension"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for building Dhara extensions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Zosma AI", email = "dev@zosma.ai" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
keywords = ["dhara", "extension", "json-rpc", "ai", "coding-agent"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dhara-extension — Python SDK for building Dhara extensions.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Extension class with tool registration and JSON-RPC loop
|
|
6
|
+
- Protocol helpers for message parsing and serialization
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .extension import Extension, create_extension, ToolHandler, ToolDefinition
|
|
10
|
+
from .protocol import (
|
|
11
|
+
ErrorCodes,
|
|
12
|
+
JsonRpcRequest,
|
|
13
|
+
JsonRpcSuccess,
|
|
14
|
+
JsonRpcErrorResponse,
|
|
15
|
+
parse_message,
|
|
16
|
+
serialize_message,
|
|
17
|
+
create_success,
|
|
18
|
+
create_error,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Extension",
|
|
23
|
+
"create_extension",
|
|
24
|
+
"ToolHandler",
|
|
25
|
+
"ToolDefinition",
|
|
26
|
+
"ErrorCodes",
|
|
27
|
+
"JsonRpcRequest",
|
|
28
|
+
"JsonRpcSuccess",
|
|
29
|
+
"JsonRpcErrorResponse",
|
|
30
|
+
"parse_message",
|
|
31
|
+
"serialize_message",
|
|
32
|
+
"create_success",
|
|
33
|
+
"create_error",
|
|
34
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main extension class for building Dhara extensions in Python.
|
|
3
|
+
|
|
4
|
+
Provides a JSON-RPC stdin/stdout loop with automatic protocol handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from .protocol import ErrorCodes
|
|
14
|
+
|
|
15
|
+
# ── Type aliases ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
ToolHandler = Callable[[dict[str, Any]], dict[str, Any]]
|
|
18
|
+
ToolDefinition = dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Extension class ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Extension:
|
|
25
|
+
"""
|
|
26
|
+
A Dhara extension that communicates via JSON-RPC over stdin/stdout.
|
|
27
|
+
|
|
28
|
+
Usage::
|
|
29
|
+
|
|
30
|
+
ext = Extension(name="my-ext", version="1.0.0")
|
|
31
|
+
|
|
32
|
+
@ext.tool(
|
|
33
|
+
name="hello",
|
|
34
|
+
description="Say hello",
|
|
35
|
+
parameters={"type": "object", "properties": {"name": {"type": "string"}}},
|
|
36
|
+
)
|
|
37
|
+
def hello(input: dict) -> dict:
|
|
38
|
+
name = input.get("name", "world")
|
|
39
|
+
return {"content": [{"type": "text", "text": f"Hello, {name}!"}]}
|
|
40
|
+
|
|
41
|
+
ext.run()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
name: str = "python-ext",
|
|
47
|
+
version: str = "1.0.0",
|
|
48
|
+
*,
|
|
49
|
+
description: str = "",
|
|
50
|
+
debug: bool = False,
|
|
51
|
+
):
|
|
52
|
+
self.name = name
|
|
53
|
+
self.version = version
|
|
54
|
+
self.description = description
|
|
55
|
+
self.debug = debug
|
|
56
|
+
self._tools: dict[str, tuple[ToolDefinition, ToolHandler]] = {}
|
|
57
|
+
self._shutdown_requested = False
|
|
58
|
+
|
|
59
|
+
# ── Tool registration ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def tool(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
description: str = "",
|
|
65
|
+
parameters: dict[str, Any] | None = None,
|
|
66
|
+
capabilities: list[str] | None = None,
|
|
67
|
+
) -> Callable[[ToolHandler], ToolHandler]:
|
|
68
|
+
"""
|
|
69
|
+
Decorator that registers a tool handler.
|
|
70
|
+
"""
|
|
71
|
+
def decorator(handler: ToolHandler) -> ToolHandler:
|
|
72
|
+
tool_def: ToolDefinition = {
|
|
73
|
+
"name": name,
|
|
74
|
+
"description": description,
|
|
75
|
+
"parameters": parameters or {"type": "object", "properties": {}},
|
|
76
|
+
}
|
|
77
|
+
if capabilities:
|
|
78
|
+
tool_def["capabilities"] = capabilities
|
|
79
|
+
self._tools[name] = (tool_def, handler)
|
|
80
|
+
return handler
|
|
81
|
+
return decorator
|
|
82
|
+
|
|
83
|
+
# ── Message dispatch ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def _dispatch(self, raw_line: str) -> str | None:
|
|
86
|
+
"""
|
|
87
|
+
Dispatch a single JSON-RPC request.
|
|
88
|
+
Returns a JSON response string, or None for notifications.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(raw_line)
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
return json.dumps(
|
|
94
|
+
{"jsonrpc": "2.0", "id": None, "error": {"code": ErrorCodes.PARSE_ERROR, "message": "Invalid JSON"}},
|
|
95
|
+
separators=(",", ":"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
method = data.get("method", "")
|
|
99
|
+
msg_id = data.get("id")
|
|
100
|
+
|
|
101
|
+
if method == "initialize":
|
|
102
|
+
resp = {
|
|
103
|
+
"protocolVersion": "0.1.0",
|
|
104
|
+
"name": self.name,
|
|
105
|
+
"version": self.version,
|
|
106
|
+
"description": self.description,
|
|
107
|
+
"tools": [defn for defn, _ in self._tools.values()],
|
|
108
|
+
}
|
|
109
|
+
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": resp}, separators=(",", ":"))
|
|
110
|
+
|
|
111
|
+
if method == "tools/execute":
|
|
112
|
+
params = data.get("params", {})
|
|
113
|
+
tool_name = params.get("toolName", "")
|
|
114
|
+
tool_input = params.get("input", {})
|
|
115
|
+
|
|
116
|
+
if tool_name not in self._tools:
|
|
117
|
+
return json.dumps(
|
|
118
|
+
{
|
|
119
|
+
"jsonrpc": "2.0",
|
|
120
|
+
"id": msg_id,
|
|
121
|
+
"error": {"code": ErrorCodes.TOOL_NOT_FOUND, "message": f"Tool not found: {tool_name}"},
|
|
122
|
+
},
|
|
123
|
+
separators=(",", ":"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
_defn, handler = self._tools[tool_name]
|
|
127
|
+
try:
|
|
128
|
+
result = handler(tool_input)
|
|
129
|
+
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}, separators=(",", ":"))
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
return json.dumps(
|
|
132
|
+
{
|
|
133
|
+
"jsonrpc": "2.0",
|
|
134
|
+
"id": msg_id,
|
|
135
|
+
"error": {"code": ErrorCodes.TOOL_EXECUTION_ERROR, "message": f"Tool error: {exc}"},
|
|
136
|
+
},
|
|
137
|
+
separators=(",", ":"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if method == "shutdown":
|
|
141
|
+
self._shutdown_requested = True
|
|
142
|
+
return json.dumps(
|
|
143
|
+
{"jsonrpc": "2.0", "id": msg_id, "result": {"status": "ok"}},
|
|
144
|
+
separators=(",", ":"),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Unknown method
|
|
148
|
+
return json.dumps(
|
|
149
|
+
{
|
|
150
|
+
"jsonrpc": "2.0",
|
|
151
|
+
"id": msg_id,
|
|
152
|
+
"error": {"code": ErrorCodes.METHOD_NOT_FOUND, "message": f"Unknown: {method}"},
|
|
153
|
+
},
|
|
154
|
+
separators=(",", ":"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ── Main loop ────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
def run(self) -> None:
|
|
160
|
+
"""Run the extension main loop (stdin → stdout JSON-RPC)."""
|
|
161
|
+
if self.debug:
|
|
162
|
+
sys.stderr.write(f"[dhara:{self.name}] started\n")
|
|
163
|
+
|
|
164
|
+
for line in sys.stdin:
|
|
165
|
+
line = line.strip()
|
|
166
|
+
if not line:
|
|
167
|
+
continue
|
|
168
|
+
if self.debug:
|
|
169
|
+
sys.stderr.write(f"[dhara:{self.name}] << {line}\n")
|
|
170
|
+
response = self._dispatch(line)
|
|
171
|
+
if response:
|
|
172
|
+
print(response, flush=True)
|
|
173
|
+
if self._shutdown_requested:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if self.debug:
|
|
177
|
+
sys.stderr.write(f"[dhara:{self.name}] stopped\n")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_extension(
|
|
181
|
+
name: str = "python-ext",
|
|
182
|
+
version: str = "1.0.0",
|
|
183
|
+
description: str = "",
|
|
184
|
+
) -> Extension:
|
|
185
|
+
"""Create a new Dhara extension instance."""
|
|
186
|
+
return Extension(name=name, version=version, description=description)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 message types for the Dhara extension protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ── JSON-RPC message types ──────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class JsonRpcRequest:
|
|
14
|
+
"""A JSON-RPC 2.0 request from the core."""
|
|
15
|
+
|
|
16
|
+
jsonrpc: str = "2.0"
|
|
17
|
+
id: int | str | None = None
|
|
18
|
+
method: str = ""
|
|
19
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class JsonRpcSuccess:
|
|
24
|
+
"""A successful JSON-RPC 2.0 response."""
|
|
25
|
+
|
|
26
|
+
jsonrpc: str = "2.0"
|
|
27
|
+
id: int | str | None = None
|
|
28
|
+
result: Any = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class JsonRpcError:
|
|
33
|
+
"""A JSON-RPC 2.0 error object."""
|
|
34
|
+
|
|
35
|
+
code: int = -32603
|
|
36
|
+
message: str = "Internal error"
|
|
37
|
+
data: Any = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class JsonRpcErrorResponse:
|
|
42
|
+
"""An error JSON-RPC 2.0 response."""
|
|
43
|
+
|
|
44
|
+
jsonrpc: str = "2.0"
|
|
45
|
+
id: int | str | None = None
|
|
46
|
+
error: JsonRpcError | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
JsonRpcMessage = JsonRpcRequest | JsonRpcSuccess | JsonRpcErrorResponse
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Standard error codes (mirrors @zosmaai/dhara-extension) ────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ErrorCodes:
|
|
56
|
+
"""Standard JSON-RPC error codes used by Dhara."""
|
|
57
|
+
|
|
58
|
+
PARSE_ERROR = -32700
|
|
59
|
+
INVALID_REQUEST = -32600
|
|
60
|
+
METHOD_NOT_FOUND = -32601
|
|
61
|
+
INVALID_PARAMS = -32602
|
|
62
|
+
INTERNAL_ERROR = -32603
|
|
63
|
+
|
|
64
|
+
# Dhara-specific codes
|
|
65
|
+
TOOL_EXECUTION_ERROR = -32000
|
|
66
|
+
TOOL_NOT_FOUND = -32001
|
|
67
|
+
CAPABILITY_DENIED = -32002
|
|
68
|
+
EXTENSION_CRASHED = -32003
|
|
69
|
+
HANDSHAKE_TIMEOUT = -32004
|
|
70
|
+
SHUTDOWN = -32005
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Dhara protocol message helpers ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_message(line: str) -> JsonRpcMessage:
|
|
77
|
+
"""Parse a single JSON-RPC message from a JSON string."""
|
|
78
|
+
import json
|
|
79
|
+
|
|
80
|
+
data = json.loads(line)
|
|
81
|
+
|
|
82
|
+
if "method" in data:
|
|
83
|
+
return JsonRpcRequest(
|
|
84
|
+
jsonrpc=data.get("jsonrpc", "2.0"),
|
|
85
|
+
id=data.get("id"),
|
|
86
|
+
method=data["method"],
|
|
87
|
+
params=data.get("params", {}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if "error" in data:
|
|
91
|
+
err = data.get("error", {})
|
|
92
|
+
return JsonRpcErrorResponse(
|
|
93
|
+
jsonrpc=data.get("jsonrpc", "2.0"),
|
|
94
|
+
id=data.get("id"),
|
|
95
|
+
error=JsonRpcError(
|
|
96
|
+
code=err.get("code", -32603),
|
|
97
|
+
message=err.get("message", "Unknown error"),
|
|
98
|
+
data=err.get("data"),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return JsonRpcSuccess(
|
|
103
|
+
jsonrpc=data.get("jsonrpc", "2.0"),
|
|
104
|
+
id=data.get("id"),
|
|
105
|
+
result=data.get("result"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def serialize_message(message: JsonRpcMessage) -> str:
|
|
110
|
+
"""Serialize a JSON-RPC message to a JSON string."""
|
|
111
|
+
import json
|
|
112
|
+
|
|
113
|
+
if isinstance(message, JsonRpcRequest):
|
|
114
|
+
obj: dict[str, Any] = {
|
|
115
|
+
"jsonrpc": message.jsonrpc,
|
|
116
|
+
"method": message.method,
|
|
117
|
+
}
|
|
118
|
+
if message.id is not None:
|
|
119
|
+
obj["id"] = message.id
|
|
120
|
+
if message.params:
|
|
121
|
+
obj["params"] = message.params
|
|
122
|
+
return json.dumps(obj, separators=(",", ":"))
|
|
123
|
+
|
|
124
|
+
if isinstance(message, JsonRpcSuccess):
|
|
125
|
+
obj = {
|
|
126
|
+
"jsonrpc": message.jsonrpc,
|
|
127
|
+
"id": message.id,
|
|
128
|
+
"result": message.result,
|
|
129
|
+
}
|
|
130
|
+
return json.dumps(obj, separators=(",", ":"))
|
|
131
|
+
|
|
132
|
+
if isinstance(message, JsonRpcErrorResponse):
|
|
133
|
+
obj = {
|
|
134
|
+
"jsonrpc": message.jsonrpc,
|
|
135
|
+
"id": message.id,
|
|
136
|
+
"error": {
|
|
137
|
+
"code": message.error.code if message.error else ErrorCodes.INTERNAL_ERROR,
|
|
138
|
+
"message": message.error.message if message.error else "Unknown error",
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
if message.error and message.error.data:
|
|
142
|
+
obj["error"]["data"] = message.error.data
|
|
143
|
+
return json.dumps(obj, separators=(",", ":"))
|
|
144
|
+
|
|
145
|
+
return json.dumps(message, separators=(",", ":"))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_success(id: int | str | None, result: Any) -> JsonRpcSuccess:
|
|
149
|
+
"""Create a success response."""
|
|
150
|
+
return JsonRpcSuccess(id=id, result=result)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_error(
|
|
154
|
+
id: int | str | None,
|
|
155
|
+
code: int = ErrorCodes.INTERNAL_ERROR,
|
|
156
|
+
message: str = "Internal error",
|
|
157
|
+
data: Any = None,
|
|
158
|
+
) -> JsonRpcErrorResponse:
|
|
159
|
+
"""Create an error response."""
|
|
160
|
+
return JsonRpcErrorResponse(id=id, error=JsonRpcError(code=code, message=message, data=data))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhara-extension
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for building Dhara extensions
|
|
5
|
+
Author-email: Zosma AI <dev@zosma.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: dhara,extension,json-rpc,ai,coding-agent
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# dhara-extension-py
|
|
20
|
+
|
|
21
|
+
Python SDK for building [Dhara](https://github.com/zosmaai/dhara) extensions.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install dhara-extension
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install from source:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd packages/dhara-extension-py
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
Create a file `hello-ext/main.py`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
#!/usr/bin/env python3
|
|
42
|
+
"""A simple Dhara extension using the Python SDK."""
|
|
43
|
+
|
|
44
|
+
from dhara_extension import Extension
|
|
45
|
+
|
|
46
|
+
ext = Extension(
|
|
47
|
+
name="hello-ext",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description="A friendly hello extension",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@ext.tool(
|
|
53
|
+
name="hello",
|
|
54
|
+
description="Greet someone by name",
|
|
55
|
+
parameters={
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"name": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Name to greet",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"required": ["name"],
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
def hello(input_data):
|
|
67
|
+
name = input_data.get("name", "World")
|
|
68
|
+
greeting = f"Hello, {name}! Welcome to Dhara."
|
|
69
|
+
return {
|
|
70
|
+
"content": [
|
|
71
|
+
{"type": "text", "text": greeting},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ext.tool(
|
|
76
|
+
name="count",
|
|
77
|
+
description="Count items in a list",
|
|
78
|
+
)
|
|
79
|
+
def count(input_data):
|
|
80
|
+
items = input_data.get("items", [])
|
|
81
|
+
return {
|
|
82
|
+
"content": [
|
|
83
|
+
{"type": "text", "text": f"Counted {len(items)} items."},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
ext.run()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
And a `manifest.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"name": "hello-ext",
|
|
96
|
+
"version": "1.0.0",
|
|
97
|
+
"runtime": {
|
|
98
|
+
"type": "subprocess",
|
|
99
|
+
"command": "python3 main.py",
|
|
100
|
+
"protocol": "json-rpc"
|
|
101
|
+
},
|
|
102
|
+
"provides": {
|
|
103
|
+
"tools": ["hello", "count"]
|
|
104
|
+
},
|
|
105
|
+
"capabilities": []
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `Extension(name, version, description="", debug=False)`
|
|
112
|
+
|
|
113
|
+
Main extension class.
|
|
114
|
+
|
|
115
|
+
- **`tool(name, description, parameters, capabilities)`** — Decorator to register a tool handler
|
|
116
|
+
- **`run()`** — Start the JSON-RPC stdin/stdout loop
|
|
117
|
+
|
|
118
|
+
### `create_extension(name, version, description="")`
|
|
119
|
+
|
|
120
|
+
Convenience function to create an `Extension` instance.
|
|
121
|
+
|
|
122
|
+
## Protocol
|
|
123
|
+
|
|
124
|
+
Extensions communicate with Dhara via JSON-RPC 2.0 over stdin/stdout:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
|
|
128
|
+
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"0.1.0","name":"hello-ext","tools":[...]}}
|
|
129
|
+
|
|
130
|
+
→ {"jsonrpc":"2.0","id":2,"method":"tools/execute",
|
|
131
|
+
"params":{"toolName":"hello","input":{"name":"World"}}}
|
|
132
|
+
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hello, World!"}]}}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
See the [Dhara Extension Protocol](https://github.com/zosmaai/dhara/blob/main/spec/extension-protocol.md) for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/dhara_extension/__init__.py
|
|
4
|
+
src/dhara_extension/extension.py
|
|
5
|
+
src/dhara_extension/protocol.py
|
|
6
|
+
src/dhara_extension.egg-info/PKG-INFO
|
|
7
|
+
src/dhara_extension.egg-info/SOURCES.txt
|
|
8
|
+
src/dhara_extension.egg-info/dependency_links.txt
|
|
9
|
+
src/dhara_extension.egg-info/top_level.txt
|
|
10
|
+
tests/test_extension.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dhara_extension
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the Dhara Python SDK."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestProtocolHelpers(unittest.TestCase):
|
|
11
|
+
"""Test protocol.py message helpers."""
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
from dhara_extension.protocol import parse_message, serialize_message, create_success, create_error
|
|
15
|
+
import json as _json
|
|
16
|
+
self._json = _json
|
|
17
|
+
self.parse = parse_message
|
|
18
|
+
self.serialize = serialize_message
|
|
19
|
+
self.success = create_success
|
|
20
|
+
self.error = create_error
|
|
21
|
+
|
|
22
|
+
def test_parse_request(self):
|
|
23
|
+
msg = self.parse('{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"0.1.0"}}')
|
|
24
|
+
# parse_message returns dataclass objects
|
|
25
|
+
self.assertEqual(msg.method, "initialize")
|
|
26
|
+
self.assertEqual(msg.id, 1)
|
|
27
|
+
|
|
28
|
+
def test_parse_success(self):
|
|
29
|
+
msg = self.parse('{"jsonrpc":"2.0","id":1,"result":{"status":"ok"}}')
|
|
30
|
+
self.assertEqual(msg.result, {"status": "ok"})
|
|
31
|
+
|
|
32
|
+
def test_parse_error(self):
|
|
33
|
+
msg = self.parse('{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Not found"}}')
|
|
34
|
+
self.assertEqual(msg.error.code, -32601)
|
|
35
|
+
|
|
36
|
+
def test_serialize_success(self):
|
|
37
|
+
result = self.success(1, {"tools": []})
|
|
38
|
+
raw = self.serialize(result)
|
|
39
|
+
data = json.loads(raw)
|
|
40
|
+
self.assertEqual(data["id"], 1)
|
|
41
|
+
self.assertEqual(data["result"]["tools"], [])
|
|
42
|
+
|
|
43
|
+
def test_serialize_error(self):
|
|
44
|
+
err = self.error(1, code=-32000, message="Tool error")
|
|
45
|
+
raw = self.serialize(err)
|
|
46
|
+
data = json.loads(raw)
|
|
47
|
+
self.assertEqual(data["id"], 1)
|
|
48
|
+
self.assertEqual(data["error"]["code"], -32000)
|
|
49
|
+
|
|
50
|
+
def test_create_extension(self):
|
|
51
|
+
from dhara_extension import create_extension
|
|
52
|
+
ext = create_extension("test-ext", "1.0.0", "A test extension")
|
|
53
|
+
self.assertEqual(ext.name, "test-ext")
|
|
54
|
+
self.assertEqual(ext.version, "1.0.0")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestExtensionTools(unittest.TestCase):
|
|
58
|
+
"""Test tool registration and dispatch."""
|
|
59
|
+
|
|
60
|
+
def setUp(self):
|
|
61
|
+
from dhara_extension import Extension
|
|
62
|
+
self.ext = Extension("test-ext", "1.0.0", description="A test extension")
|
|
63
|
+
|
|
64
|
+
@self.ext.tool(
|
|
65
|
+
name="echo",
|
|
66
|
+
description="Echo input",
|
|
67
|
+
parameters={"type": "object", "properties": {"message": {"type": "string"}}},
|
|
68
|
+
)
|
|
69
|
+
def echo(input_data):
|
|
70
|
+
msg = input_data.get("message", "")
|
|
71
|
+
return {"content": [{"type": "text", "text": msg}]}
|
|
72
|
+
|
|
73
|
+
self.echo_handler = echo
|
|
74
|
+
|
|
75
|
+
def test_tool_registration(self):
|
|
76
|
+
self.assertIn("echo", self.ext._tools)
|
|
77
|
+
|
|
78
|
+
def test_initialize_response(self):
|
|
79
|
+
import json
|
|
80
|
+
resp = self.ext._dispatch('{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}')
|
|
81
|
+
data = json.loads(resp)
|
|
82
|
+
self.assertEqual(data["id"], 1)
|
|
83
|
+
self.assertIn("tools", data["result"])
|
|
84
|
+
tool_names = [t["name"] for t in data["result"]["tools"]]
|
|
85
|
+
self.assertIn("echo", tool_names)
|
|
86
|
+
|
|
87
|
+
def test_tool_execution(self):
|
|
88
|
+
import json
|
|
89
|
+
resp = self.ext._dispatch(
|
|
90
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/execute","params":{"toolName":"echo","input":{"message":"hello"}}}'
|
|
91
|
+
)
|
|
92
|
+
data = json.loads(resp)
|
|
93
|
+
self.assertEqual(data["id"], 2)
|
|
94
|
+
self.assertEqual(data["result"]["content"][0]["text"], "hello")
|
|
95
|
+
|
|
96
|
+
def test_unknown_tool(self):
|
|
97
|
+
import json
|
|
98
|
+
resp = self.ext._dispatch(
|
|
99
|
+
'{"jsonrpc":"2.0","id":3,"method":"tools/execute","params":{"toolName":"nonexistent","input":{}}}'
|
|
100
|
+
)
|
|
101
|
+
data = json.loads(resp)
|
|
102
|
+
self.assertIn("error", data)
|
|
103
|
+
self.assertIn("not found", data["error"]["message"])
|
|
104
|
+
|
|
105
|
+
def test_shutdown(self):
|
|
106
|
+
import json
|
|
107
|
+
resp = self.ext._dispatch('{"jsonrpc":"2.0","id":4,"method":"shutdown"}')
|
|
108
|
+
data = json.loads(resp)
|
|
109
|
+
self.assertEqual(data["result"]["status"], "ok")
|
|
110
|
+
self.assertTrue(self.ext._shutdown_requested)
|
|
111
|
+
|
|
112
|
+
def test_unknown_method(self):
|
|
113
|
+
import json
|
|
114
|
+
resp = self.ext._dispatch('{"jsonrpc":"2.0","id":5,"method":"bogus","params":{}}')
|
|
115
|
+
data = json.loads(resp)
|
|
116
|
+
self.assertIn("error", data)
|
|
117
|
+
self.assertIn("Unknown", data["error"]["message"])
|
|
118
|
+
|
|
119
|
+
def test_invalid_json(self):
|
|
120
|
+
import json
|
|
121
|
+
resp = self.ext._dispatch("not json")
|
|
122
|
+
data = json.loads(resp)
|
|
123
|
+
self.assertIn("error", data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|