geochat-sdk 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Araz Shah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: geochat-sdk
3
+ Version: 1.0.0
4
+ Summary: Lightweight SDK for building GeoChat Spatial AI plugins
5
+ Author: Araz Shah
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arazshah/geochat-platform
8
+ Project-URL: Repository, https://github.com/arazshah/geochat-platform
9
+ Project-URL: Issues, https://github.com/arazshah/geochat-platform/issues
10
+ Keywords: geospatial,gis,plugins,spatial-analysis,geoai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: GIS
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Requires-Dist: geochat-kernel>=1.0.0
25
+ Dynamic: license-file
26
+
27
+ # GeoChat SDK
28
+ This SDK provides decorators and domain models to build trusted spatial plugins for the GeoChat Core Engine.
@@ -0,0 +1,2 @@
1
+ # GeoChat SDK
2
+ This SDK provides decorators and domain models to build trusted spatial plugins for the GeoChat Core Engine.
@@ -0,0 +1,33 @@
1
+ """
2
+ GeoChat SDK public API.
3
+
4
+ This __init__.py is aligned with the current SDK implementation:
5
+ - capability is defined in geochat_sdk.decorators
6
+ - auto_collect and SDKPlugin are defined in geochat_sdk.plugin
7
+ - Raster/Vector helper types are defined in geochat_sdk.types
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from geochat_sdk.decorators import capability
13
+ from geochat_sdk.plugin import auto_collect, SDKPlugin
14
+ from geochat_sdk.types.raster import RasterIn, RasterOut
15
+ from geochat_sdk.types.vector import VectorIn, VectorOut
16
+ from geochat_sdk.exceptions import SDKError, SDKDependencyError, SDKValidationError
17
+
18
+ __all__ = [
19
+ "capability",
20
+ "auto_collect",
21
+ "SDKPlugin",
22
+ "RasterIn",
23
+ "RasterOut",
24
+ "VectorIn",
25
+ "VectorOut",
26
+ "SDKError",
27
+ "SDKDependencyError",
28
+ "SDKValidationError",
29
+ ]
30
+
31
+ # Must match [project].version in ../pyproject.toml - that file is the
32
+ # source of truth for what PyPI publishes.
33
+ __version__ = "1.0.0"
@@ -0,0 +1,89 @@
1
+ # geochat_sdk/decorators.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable
5
+
6
+ from geochat_kernel.models.capability import CapabilityDescriptor
7
+
8
+
9
+ class CapabilityRegistration:
10
+ """
11
+ Internal registry descriptor for a decorated capability.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ func: Callable,
17
+ name: str,
18
+ keywords: list[str] | None = None,
19
+ description: str | None = None,
20
+ required_inputs: list[str] | None = None,
21
+ optional_inputs: list[str] | None = None,
22
+ output_kind: str | None = None,
23
+ requires_permissions: list[str] | None = None,
24
+ metadata: dict[str, Any] | None = None,
25
+ ) -> None:
26
+ self.func = func
27
+ self.name = name
28
+ self.keywords = keywords or []
29
+ self.description = description or func.__doc__ or f"Geospatial capability: {name}"
30
+ self.required_inputs = required_inputs or []
31
+ self.optional_inputs = optional_inputs or []
32
+ self.output_kind = output_kind or "payload"
33
+ self.requires_permissions = requires_permissions or []
34
+ self.metadata = metadata or {}
35
+
36
+ def build_descriptor(self, plugin_id: str) -> CapabilityDescriptor:
37
+ return CapabilityDescriptor(
38
+ name=self.name,
39
+ kind="capability",
40
+ plugin_id=plugin_id,
41
+ component_name=f"handler_{plugin_id}_{self.func.__name__}",
42
+ description=self.description,
43
+ keywords=self.keywords,
44
+ required_inputs=self.required_inputs,
45
+ optional_inputs=self.optional_inputs,
46
+ output_kind=self.output_kind,
47
+ requires_permissions=self.requires_permissions,
48
+ metadata={**self.metadata, "routable": True},
49
+ )
50
+
51
+
52
+ _PENDING_CAPABILITIES: list[CapabilityRegistration] = []
53
+
54
+
55
+ def capability(
56
+ name: str,
57
+ *,
58
+ keywords: list[str] | None = None,
59
+ description: str | None = None,
60
+ required_inputs: list[str] | None = None,
61
+ optional_inputs: list[str] | None = None,
62
+ output_kind: str | None = None,
63
+ permissions: list[Any] | None = None,
64
+ metadata: dict[str, Any] | None = None,
65
+ ) -> Callable[[Callable], Callable]:
66
+ """
67
+ Declare a function as a routable geospatial capability.
68
+ """
69
+ def decorator(func: Callable) -> Callable:
70
+ perm_list = []
71
+ if permissions:
72
+ for p in permissions:
73
+ perm_list.append(p.value if hasattr(p, "value") else str(p))
74
+
75
+ reg = CapabilityRegistration(
76
+ func=func,
77
+ name=name,
78
+ keywords=keywords,
79
+ description=description,
80
+ required_inputs=required_inputs,
81
+ optional_inputs=optional_inputs,
82
+ output_kind=output_kind,
83
+ requires_permissions=perm_list,
84
+ metadata=metadata,
85
+ )
86
+ _PENDING_CAPABILITIES.append(reg)
87
+ setattr(func, "__geochat_capability__", reg)
88
+ return func
89
+ return decorator
@@ -0,0 +1,13 @@
1
+ # geochat_sdk/exceptions.py
2
+ from __future__ import annotations
3
+
4
+ class SDKError(Exception):
5
+ """Base exception for all GeoChat SDK errors."""
6
+
7
+
8
+ class SDKDependencyError(SDKError):
9
+ """Raised when a geospatial library (e.g., rasterio, geopandas) is required but missing."""
10
+
11
+
12
+ class SDKValidationError(SDKError):
13
+ """Raised when a capability function signature, type, or mapping is invalid."""
@@ -0,0 +1,351 @@
1
+ # geochat_sdk/plugin.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import typing
6
+ from typing import Any, Callable, get_type_hints
7
+
8
+ from geochat_kernel.contracts.planner import BasePlanner
9
+ from geochat_kernel.contracts.plugin import BasePlugin
10
+ from geochat_kernel.contracts.result_fusion import BaseResultFusion
11
+ from geochat_kernel.contracts.step_handler import BaseStepHandler
12
+ from geochat_kernel.models.execution_artifact import ExecutionArtifact
13
+ from geochat_kernel.models.geo_response import GeoResponse
14
+ from geochat_kernel.models.manifest import PluginManifest
15
+ from geochat_kernel.models.query_ir import QueryIR
16
+ from geochat_kernel.models.query_plan import PlanStep, QueryPlan
17
+ from geochat_kernel.runtime.execution_context import ExecutionContext
18
+
19
+ from geochat_sdk.decorators import CapabilityRegistration, _PENDING_CAPABILITIES
20
+ from geochat_sdk.types.raster import RasterIn, RasterOut
21
+ from geochat_sdk.types.vector import VectorIn, VectorOut
22
+
23
+
24
+ class SDKStepHandler(BaseStepHandler):
25
+ """
26
+ Dynamic Step Handler generated by the SDK.
27
+ Performs automatic type coercion for arguments and return values.
28
+ """
29
+
30
+ def __init__(self, plugin_id: str, reg: CapabilityRegistration) -> None:
31
+ self._plugin_id = plugin_id
32
+ self._reg = reg
33
+ self._step_type = f"sdk.{plugin_id}.{reg.name}"
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ return f"handler_{self._plugin_id}_{self._reg.func.__name__}"
38
+
39
+ @property
40
+ def handled_types(self) -> list[str]:
41
+ return [self._step_type]
42
+
43
+ @property
44
+ def priority(self) -> int:
45
+ return 100
46
+
47
+ async def handle(
48
+ self,
49
+ step: PlanStep,
50
+ inputs: dict[str, ExecutionArtifact],
51
+ context: ExecutionContext,
52
+ ) -> ExecutionArtifact:
53
+ sig = inspect.signature(self._reg.func)
54
+ kwargs: dict[str, Any] = {}
55
+
56
+ # ── کلید اصلاح: resolve کردن annotation‌های رشته‌ای به نوع واقعی ──
57
+ try:
58
+ resolved_hints = get_type_hints(self._reg.func)
59
+ except Exception:
60
+ # اگر resolve ناموفق بود، از annotation خام استفاده می‌کنیم
61
+ resolved_hints = {
62
+ p: param.annotation
63
+ for p, param in sig.parameters.items()
64
+ if param.annotation is not inspect.Parameter.empty
65
+ }
66
+
67
+ for param_name, param in sig.parameters.items():
68
+ # annotation واقعی (نه رشته) را از resolved_hints می‌گیریم
69
+ annotation = resolved_hints.get(param_name, param.annotation)
70
+
71
+ # 1. Map context
72
+ if annotation is ExecutionContext or param_name == "context":
73
+ kwargs[param_name] = context
74
+ continue
75
+
76
+ # 2. Map RasterIn
77
+ if annotation is RasterIn:
78
+ art = inputs.get(param_name)
79
+ if not art:
80
+ art = next((a for a in inputs.values() if a.kind == "raster_ref"), None)
81
+ if not art:
82
+ available = context.metadata.get("available_inputs", [])
83
+ if param_name in available or any(param_name in str(x) for x in available) or "band" in param_name:
84
+ art = ExecutionArtifact.of_payload(
85
+ kind="raster_ref",
86
+ payload={"path": f"data/{param_name}.tif", "name": param_name, "virtual": True},
87
+ produced_by="sdk_context_resolver",
88
+ )
89
+ else:
90
+ raise ValueError(f"Missing raster input for argument '{param_name}'")
91
+ kwargs[param_name] = RasterIn(art)
92
+ continue
93
+
94
+ # 3. Map VectorIn
95
+ if annotation is VectorIn:
96
+ art = inputs.get(param_name)
97
+ if not art:
98
+ art = next((a for a in inputs.values() if a.kind in ("features", "vector")), None)
99
+ if not art:
100
+ available = context.metadata.get("available_inputs", [])
101
+ if param_name in available or any(param_name in str(x) for x in available):
102
+ art = ExecutionArtifact.of_payload(
103
+ kind="features",
104
+ payload={"features": [], "name": param_name, "virtual": True},
105
+ produced_by="sdk_context_resolver",
106
+ )
107
+ else:
108
+ raise ValueError(f"Missing vector input for argument '{param_name}'")
109
+ kwargs[param_name] = VectorIn(art)
110
+ continue
111
+
112
+ # 4. Map primitives from step parameters
113
+ if param_name in step.parameters:
114
+ kwargs[param_name] = step.parameters[param_name]
115
+ elif param.default is not inspect.Parameter.empty:
116
+ kwargs[param_name] = param.default
117
+ else:
118
+ raise ValueError(
119
+ f"Missing parameter '{param_name}' required by capability '{self._reg.name}'."
120
+ )
121
+
122
+ # Execute target capability
123
+ if inspect.iscoroutinefunction(self._reg.func):
124
+ result = await self._reg.func(**kwargs)
125
+ else:
126
+ result = self._reg.func(**kwargs)
127
+
128
+ # Coerce output back to ExecutionArtifact
129
+ if isinstance(result, RasterOut):
130
+ return result.to_artifact(produced_by=self.name)
131
+ if isinstance(result, VectorOut):
132
+ return result.to_artifact(produced_by=self.name)
133
+ if isinstance(result, ExecutionArtifact):
134
+ return result
135
+
136
+ # Default fallback
137
+ return ExecutionArtifact.of_payload(
138
+ kind="payload",
139
+ payload={"result": result},
140
+ produced_by=self.name,
141
+ )
142
+
143
+
144
+ class SDKPlanner(BasePlanner):
145
+ """
146
+ Dynamic Planner generated by the SDK.
147
+ Routes queries to the generated Step Handler.
148
+ """
149
+
150
+ def __init__(self, plugin_id: str, reg: CapabilityRegistration) -> None:
151
+ self._plugin_id = plugin_id
152
+ self._reg = reg
153
+ self._step_type = f"sdk.{plugin_id}.{reg.name}"
154
+
155
+ @property
156
+ def name(self) -> str:
157
+ return f"planner_{self._plugin_id}_{self._reg.func.__name__}"
158
+
159
+ @property
160
+ def priority(self) -> int:
161
+ return 100
162
+
163
+ def match_score(self, query_ir: QueryIR, context: ExecutionContext) -> float:
164
+ routing = context.metadata.get("routing") or {}
165
+ selected = routing.get("selected") or []
166
+ if self._reg.name in selected:
167
+ return 1.0
168
+
169
+ raw = (query_ir.raw_text or "").lower()
170
+ if any(kw.lower() in raw for kw in self._reg.keywords):
171
+ return 0.8
172
+
173
+ return 0.0
174
+
175
+ async def build_plan(self, query_ir: QueryIR, context: ExecutionContext) -> QueryPlan:
176
+ params: dict[str, Any] = {}
177
+ for entity in query_ir.entities:
178
+ params[entity.role] = entity.name
179
+
180
+ if query_ir.constraints.radius_m:
181
+ params["radius_m"] = query_ir.constraints.radius_m
182
+ if query_ir.constraints.limit:
183
+ params["limit"] = query_ir.constraints.limit
184
+
185
+ step = PlanStep(
186
+ type=self._step_type,
187
+ name=f"step_{self._reg.name}",
188
+ parameters=params,
189
+ metadata={"plugin_id": self._plugin_id},
190
+ )
191
+
192
+ return QueryPlan(
193
+ query_ir_id=query_ir.id,
194
+ steps=[step],
195
+ metadata={"plugin_id": self._plugin_id, "source": self._plugin_id},
196
+ )
197
+
198
+
199
+ class SDKResultFusion(BaseResultFusion):
200
+ """
201
+ Default dynamic result fusion for SDK plugins.
202
+ """
203
+
204
+ def __init__(self, plugin_id: str) -> None:
205
+ self._plugin_id = plugin_id
206
+
207
+ @property
208
+ def name(self) -> str:
209
+ return f"fusion_{self._plugin_id}"
210
+
211
+ @property
212
+ def priority(self) -> int:
213
+ return 100
214
+
215
+ def match_score(
216
+ self,
217
+ query_ir: QueryIR,
218
+ plan: QueryPlan,
219
+ artifacts: dict[str, ExecutionArtifact],
220
+ context: ExecutionContext,
221
+ ) -> float:
222
+ if plan.metadata.get("plugin_id") == self._plugin_id:
223
+ return 1.0
224
+ return 0.0
225
+
226
+ async def fuse(
227
+ self,
228
+ query_ir: QueryIR,
229
+ plan: QueryPlan,
230
+ artifacts: dict[str, ExecutionArtifact],
231
+ context: ExecutionContext,
232
+ ) -> GeoResponse:
233
+ response = GeoResponse.success(
234
+ query_ir_id=query_ir.id,
235
+ request_id=context.request_id,
236
+ )
237
+ response.metadata["plugin_id"] = self._plugin_id
238
+ response.metadata["source"] = self._plugin_id
239
+
240
+ has_features = False
241
+ for art in artifacts.values():
242
+ if art.kind == "features":
243
+ features_list = art.payload.get("features", [])
244
+ from geochat_kernel.models.feature_group import FeatureGroup
245
+ from geochat_kernel.models.geo_feature import GeoFeature
246
+
247
+ f_objs = [GeoFeature.from_dict(f) for f in features_list]
248
+ response.features.extend(f_objs)
249
+ response.groups.append(
250
+ FeatureGroup(
251
+ id=f"{self._plugin_id}_group",
252
+ label="نتایج پردازش SDK",
253
+ features=f_objs,
254
+ )
255
+ )
256
+ has_features = True
257
+ elif art.kind == "raster_ref":
258
+ response.metadata["raster_path"] = art.payload.get("path")
259
+ response.metadata["raster_meta"] = art.payload
260
+
261
+ if has_features:
262
+ response.user_message.summary = (
263
+ f"پردازش SDK با موفقیت انجام شد. {len(response.features)} عارضه یافت شد."
264
+ )
265
+ else:
266
+ response.user_message.summary = "پردازش SDK با موفقیت انجام شد."
267
+
268
+ return response
269
+
270
+
271
+ class SDKPlugin(BasePlugin):
272
+ """
273
+ High-level Plugin class. Automates registry mappings and setup.
274
+ """
275
+
276
+ def __init__(self, manifest: PluginManifest | None = None) -> None:
277
+ self._manifest = manifest or self._build_default_manifest()
278
+ self._capabilities_regs: list[CapabilityRegistration] = []
279
+ self._collect_capabilities()
280
+
281
+ def _build_default_manifest(self) -> PluginManifest:
282
+ return PluginManifest(
283
+ id=self.__class__.__name__.lower(),
284
+ version="0.1.0",
285
+ name=self.__class__.__name__,
286
+ )
287
+
288
+ @property
289
+ def manifest(self) -> PluginManifest:
290
+ return self._manifest
291
+
292
+ def _collect_capabilities(self) -> None:
293
+ for _, attr in inspect.getmembers(self):
294
+ reg = getattr(attr, "__geochat_capability__", None)
295
+ if isinstance(reg, CapabilityRegistration):
296
+ reg.func = attr
297
+ self._capabilities_regs.append(reg)
298
+
299
+ async def register(self, container) -> None:
300
+ for reg in self._capabilities_regs:
301
+ descriptor = reg.build_descriptor(self.id)
302
+ container.capabilities.register(descriptor, replace=True)
303
+
304
+ container.planners.register_planner(
305
+ SDKPlanner(self.id, reg),
306
+ replace=True,
307
+ )
308
+
309
+ container.step_handlers.register_handler(
310
+ SDKStepHandler(self.id, reg),
311
+ replace=True,
312
+ )
313
+
314
+ if self._capabilities_regs:
315
+ container.fusions.register_fusion(
316
+ SDKResultFusion(self.id),
317
+ replace=True,
318
+ )
319
+
320
+
321
+ def auto_collect(
322
+ id: str,
323
+ version: str = "0.1.0",
324
+ name: str | None = None,
325
+ description: str | None = None,
326
+ author: str | None = None,
327
+ permissions: list[Any] | None = None,
328
+ ) -> SDKPlugin:
329
+ """
330
+ Collects all module-level decorated functions and bundles them into an SDKPlugin.
331
+ """
332
+ regs = list(_PENDING_CAPABILITIES)
333
+ _PENDING_CAPABILITIES.clear()
334
+
335
+ perm_list = []
336
+ if permissions:
337
+ for p in permissions:
338
+ perm_list.append(p.value if hasattr(p, "value") else str(p))
339
+
340
+ manifest = PluginManifest(
341
+ id=id,
342
+ version=version,
343
+ name=name or id.replace("_", " ").title(),
344
+ description=description,
345
+ author=author,
346
+ permissions=perm_list,
347
+ )
348
+
349
+ plugin = SDKPlugin(manifest)
350
+ plugin._capabilities_regs.extend(regs)
351
+ return plugin
@@ -0,0 +1,7 @@
1
+ # geochat_sdk/types/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from geochat_sdk.types.raster import RasterIn, RasterOut
5
+ from geochat_sdk.types.vector import VectorIn, VectorOut
6
+
7
+ __all__ = ["RasterIn", "RasterOut", "VectorIn", "VectorOut"]
@@ -0,0 +1,104 @@
1
+ # geochat_sdk/types/raster.py
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from geochat_kernel.models.execution_artifact import ExecutionArtifact
8
+
9
+
10
+ class RasterIn:
11
+ """
12
+ Wrapper for input Raster data.
13
+ Provides lazy-loaded helper methods for RS specialists.
14
+ """
15
+
16
+ def __init__(self, artifact: ExecutionArtifact) -> None:
17
+ self.artifact = artifact
18
+
19
+ @property
20
+ def path(self) -> str:
21
+ payload = self.artifact.payload or {}
22
+ return payload.get("path") or payload.get("url") or ""
23
+
24
+ @property
25
+ def metadata(self) -> dict[str, Any]:
26
+ return self.artifact.payload or {}
27
+
28
+ def read_numpy(self, band: int = 1) -> Any:
29
+ """Read raster band as numpy array. Lazy imports numpy and rasterio."""
30
+ try:
31
+ import numpy as np
32
+ import rasterio
33
+ except ImportError as exc:
34
+ from geochat_sdk.exceptions import SDKDependencyError
35
+ raise SDKDependencyError(
36
+ "numpy and rasterio are required to read raster data. "
37
+ "Please install them in your plugin environment."
38
+ ) from exc
39
+
40
+ if not self.path or not Path(self.path).exists():
41
+ raise FileNotFoundError(f"Raster file not found: {self.path}")
42
+
43
+ with rasterio.open(self.path) as src:
44
+ return src.read(band)
45
+
46
+ def get_profile(self) -> dict[str, Any]:
47
+ """Get raster profile metadata."""
48
+ try:
49
+ import rasterio
50
+ except ImportError as exc:
51
+ from geochat_sdk.exceptions import SDKDependencyError
52
+ raise SDKDependencyError(
53
+ "rasterio is required to read raster profile."
54
+ ) from exc
55
+
56
+ with rasterio.open(self.path) as src:
57
+ return dict(src.profile)
58
+
59
+
60
+ class RasterOut:
61
+ """
62
+ Wrapper for output Raster data produced by a plugin.
63
+ """
64
+
65
+ def __init__(self, path: str, metadata: dict[str, Any] | None = None) -> None:
66
+ self.path = path
67
+ self.metadata = metadata or {}
68
+
69
+ @classmethod
70
+ def from_numpy(cls, data: Any, profile: dict[str, Any], output_path: str) -> RasterOut:
71
+ """Write numpy array to a GeoTIFF file using rasterio."""
72
+ try:
73
+ import numpy as np
74
+ import rasterio
75
+ except ImportError as exc:
76
+ from geochat_sdk.exceptions import SDKDependencyError
77
+ raise SDKDependencyError(
78
+ "numpy and rasterio are required to write raster data."
79
+ ) from exc
80
+
81
+ out_path = Path(output_path)
82
+ out_path.parent.mkdir(parents=True, exist_ok=True)
83
+
84
+ # Update profile with single band and correct dtype
85
+ profile.update(
86
+ count=1 if len(data.shape) == 2 else data.shape[0],
87
+ dtype=str(data.dtype),
88
+ )
89
+
90
+ with rasterio.open(str(out_path), "w", **profile) as dst:
91
+ if len(data.shape) == 2:
92
+ dst.write(data, 1)
93
+ else:
94
+ for i in range(data.shape[0]):
95
+ dst.write(data[i], i + 1)
96
+
97
+ return cls(path=str(out_path), metadata={"profile": profile})
98
+
99
+ def to_artifact(self, produced_by: str) -> ExecutionArtifact:
100
+ return ExecutionArtifact.of_payload(
101
+ kind="raster_ref",
102
+ payload={"path": self.path, **self.metadata},
103
+ produced_by=produced_by,
104
+ )
@@ -0,0 +1,78 @@
1
+ # geochat_sdk/types/vector.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ from geochat_kernel.models.execution_artifact import ExecutionArtifact
8
+
9
+
10
+ class VectorIn:
11
+ """
12
+ Wrapper for input Vector/GeoJSON data.
13
+ Provides lazy-loaded helper methods for GIS specialists.
14
+ """
15
+
16
+ def __init__(self, artifact: ExecutionArtifact) -> None:
17
+ self.artifact = artifact
18
+
19
+ @property
20
+ def features(self) -> list[dict[str, Any]]:
21
+ payload = self.artifact.payload or {}
22
+ if "features" in payload:
23
+ return payload["features"]
24
+ return []
25
+
26
+ def to_geopandas(self) -> Any:
27
+ """Convert features to a GeoPandas GeoDataFrame."""
28
+ try:
29
+ import geopandas as gpd
30
+ except ImportError as exc:
31
+ from geochat_sdk.exceptions import SDKDependencyError
32
+ raise SDKDependencyError(
33
+ "geopandas is required to convert vector data to GeoDataFrame."
34
+ ) from exc
35
+
36
+ if not self.features:
37
+ return gpd.GeoDataFrame()
38
+ return gpd.GeoDataFrame.from_features(self.features)
39
+
40
+ def to_shapely(self) -> list[Any]:
41
+ """Convert features to Shapely geometries."""
42
+ try:
43
+ from shapely.geometry import shape
44
+ except ImportError as exc:
45
+ from geochat_sdk.exceptions import SDKDependencyError
46
+ raise SDKDependencyError(
47
+ "shapely is required to parse vector geometries."
48
+ ) from exc
49
+
50
+ geoms = []
51
+ for f in self.features:
52
+ if "geometry" in f:
53
+ geoms.append(shape(f["geometry"]))
54
+ return geoms
55
+
56
+
57
+ class VectorOut:
58
+ """
59
+ Wrapper for output Vector/GeoJSON data produced by a plugin.
60
+ """
61
+
62
+ def __init__(self, features: list[dict[str, Any]], metadata: dict[str, Any] | None = None) -> None:
63
+ self.features = features
64
+ self.metadata = metadata or {}
65
+
66
+ @classmethod
67
+ def from_geopandas(cls, gdf: Any) -> VectorOut:
68
+ """Create VectorOut from a GeoDataFrame."""
69
+ geojson_str = gdf.to_json()
70
+ geojson = json.loads(geojson_str)
71
+ return cls(features=geojson.get("features", []))
72
+
73
+ def to_artifact(self, produced_by: str) -> ExecutionArtifact:
74
+ return ExecutionArtifact.of_payload(
75
+ kind="features",
76
+ payload={"features": self.features, **self.metadata},
77
+ produced_by=produced_by,
78
+ )
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: geochat-sdk
3
+ Version: 1.0.0
4
+ Summary: Lightweight SDK for building GeoChat Spatial AI plugins
5
+ Author: Araz Shah
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arazshah/geochat-platform
8
+ Project-URL: Repository, https://github.com/arazshah/geochat-platform
9
+ Project-URL: Issues, https://github.com/arazshah/geochat-platform/issues
10
+ Keywords: geospatial,gis,plugins,spatial-analysis,geoai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: GIS
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Requires-Dist: geochat-kernel>=1.0.0
25
+ Dynamic: license-file
26
+
27
+ # GeoChat SDK
28
+ This SDK provides decorators and domain models to build trusted spatial plugins for the GeoChat Core Engine.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ geochat_sdk/__init__.py
5
+ geochat_sdk/decorators.py
6
+ geochat_sdk/exceptions.py
7
+ geochat_sdk/plugin.py
8
+ geochat_sdk.egg-info/PKG-INFO
9
+ geochat_sdk.egg-info/SOURCES.txt
10
+ geochat_sdk.egg-info/dependency_links.txt
11
+ geochat_sdk.egg-info/requires.txt
12
+ geochat_sdk.egg-info/top_level.txt
13
+ geochat_sdk/types/__init__.py
14
+ geochat_sdk/types/raster.py
15
+ geochat_sdk/types/vector.py
@@ -0,0 +1,2 @@
1
+ pydantic>=2.0.0
2
+ geochat-kernel>=1.0.0
@@ -0,0 +1 @@
1
+ geochat_sdk
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "geochat-sdk"
7
+ version = "1.0.0"
8
+ description = "Lightweight SDK for building GeoChat Spatial AI plugins"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Araz Shah" }]
14
+ keywords = ["geospatial", "gis", "plugins", "spatial-analysis", "geoai"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: GIS",
25
+ ]
26
+ dependencies = [
27
+ "pydantic>=2.0.0",
28
+ # geochat_sdk imports geochat_kernel at module level in 13 places
29
+ # (decorators.py, plugin.py, types/raster.py, types/vector.py), so
30
+ # importing this package without the kernel installed fails outright.
31
+ # The dependency runs sdk -> kernel, NOT kernel -> sdk: geochat_kernel
32
+ # contains no reference to geochat_sdk at all.
33
+ "geochat-kernel>=1.0.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/arazshah/geochat-platform"
38
+ Repository = "https://github.com/arazshah/geochat-platform"
39
+ Issues = "https://github.com/arazshah/geochat-platform/issues"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["."]
43
+ include = ["geochat_sdk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+