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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ 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()