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.
- geochat_sdk-1.0.0/LICENSE +21 -0
- geochat_sdk-1.0.0/PKG-INFO +28 -0
- geochat_sdk-1.0.0/README.md +2 -0
- geochat_sdk-1.0.0/geochat_sdk/__init__.py +33 -0
- geochat_sdk-1.0.0/geochat_sdk/decorators.py +89 -0
- geochat_sdk-1.0.0/geochat_sdk/exceptions.py +13 -0
- geochat_sdk-1.0.0/geochat_sdk/plugin.py +351 -0
- geochat_sdk-1.0.0/geochat_sdk/types/__init__.py +7 -0
- geochat_sdk-1.0.0/geochat_sdk/types/raster.py +104 -0
- geochat_sdk-1.0.0/geochat_sdk/types/vector.py +78 -0
- geochat_sdk-1.0.0/geochat_sdk.egg-info/PKG-INFO +28 -0
- geochat_sdk-1.0.0/geochat_sdk.egg-info/SOURCES.txt +15 -0
- geochat_sdk-1.0.0/geochat_sdk.egg-info/dependency_links.txt +1 -0
- geochat_sdk-1.0.0/geochat_sdk.egg-info/requires.txt +2 -0
- geochat_sdk-1.0.0/geochat_sdk.egg-info/top_level.txt +1 -0
- geochat_sdk-1.0.0/pyproject.toml +43 -0
- geochat_sdk-1.0.0/setup.cfg +4 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|