aethergraph 0.1.0a2__py3-none-any.whl → 0.1.0a4__py3-none-any.whl
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.
- aethergraph/__main__.py +3 -0
- aethergraph/api/v1/artifacts.py +23 -4
- aethergraph/api/v1/schemas.py +7 -0
- aethergraph/api/v1/session.py +123 -4
- aethergraph/config/config.py +2 -0
- aethergraph/config/search.py +49 -0
- aethergraph/contracts/services/channel.py +18 -1
- aethergraph/contracts/services/execution.py +58 -0
- aethergraph/contracts/services/llm.py +26 -0
- aethergraph/contracts/services/memory.py +10 -4
- aethergraph/contracts/services/planning.py +53 -0
- aethergraph/contracts/storage/event_log.py +8 -0
- aethergraph/contracts/storage/search_backend.py +47 -0
- aethergraph/contracts/storage/vector_index.py +73 -0
- aethergraph/core/graph/action_spec.py +76 -0
- aethergraph/core/graph/graph_fn.py +75 -2
- aethergraph/core/graph/graphify.py +74 -2
- aethergraph/core/runtime/graph_runner.py +2 -1
- aethergraph/core/runtime/node_context.py +66 -3
- aethergraph/core/runtime/node_services.py +8 -0
- aethergraph/core/runtime/run_manager.py +263 -271
- aethergraph/core/runtime/run_types.py +54 -1
- aethergraph/core/runtime/runtime_env.py +35 -14
- aethergraph/core/runtime/runtime_services.py +308 -18
- aethergraph/plugins/agents/default_chat_agent.py +266 -74
- aethergraph/plugins/agents/default_chat_agent_v2.py +487 -0
- aethergraph/plugins/channel/adapters/webui.py +69 -21
- aethergraph/plugins/channel/routes/webui_routes.py +8 -48
- aethergraph/runtime/__init__.py +12 -0
- aethergraph/server/app_factory.py +10 -1
- aethergraph/server/ui_static/assets/index-CFktGdbW.js +4913 -0
- aethergraph/server/ui_static/assets/index-DcfkFlTA.css +1 -0
- aethergraph/server/ui_static/index.html +2 -2
- aethergraph/services/artifacts/facade.py +157 -21
- aethergraph/services/artifacts/types.py +35 -0
- aethergraph/services/artifacts/utils.py +42 -0
- aethergraph/services/channel/channel_bus.py +3 -1
- aethergraph/services/channel/event_hub copy.py +55 -0
- aethergraph/services/channel/event_hub.py +81 -0
- aethergraph/services/channel/factory.py +3 -2
- aethergraph/services/channel/session.py +709 -74
- aethergraph/services/container/default_container.py +69 -7
- aethergraph/services/execution/__init__.py +0 -0
- aethergraph/services/execution/local_python.py +118 -0
- aethergraph/services/indices/__init__.py +0 -0
- aethergraph/services/indices/global_indices.py +21 -0
- aethergraph/services/indices/scoped_indices.py +292 -0
- aethergraph/services/llm/generic_client.py +342 -46
- aethergraph/services/llm/generic_embed_client.py +359 -0
- aethergraph/services/llm/types.py +3 -1
- aethergraph/services/memory/distillers/llm_long_term.py +60 -109
- aethergraph/services/memory/distillers/llm_long_term_v1.py +180 -0
- aethergraph/services/memory/distillers/llm_meta_summary.py +57 -266
- aethergraph/services/memory/distillers/llm_meta_summary_v1.py +342 -0
- aethergraph/services/memory/distillers/long_term.py +48 -131
- aethergraph/services/memory/distillers/long_term_v1.py +170 -0
- aethergraph/services/memory/facade/chat.py +18 -8
- aethergraph/services/memory/facade/core.py +159 -19
- aethergraph/services/memory/facade/distillation.py +86 -31
- aethergraph/services/memory/facade/retrieval.py +100 -1
- aethergraph/services/memory/factory.py +4 -1
- aethergraph/services/planning/__init__.py +0 -0
- aethergraph/services/planning/action_catalog.py +271 -0
- aethergraph/services/planning/bindings.py +56 -0
- aethergraph/services/planning/dependency_index.py +65 -0
- aethergraph/services/planning/flow_validator.py +263 -0
- aethergraph/services/planning/graph_io_adapter.py +150 -0
- aethergraph/services/planning/input_parser.py +312 -0
- aethergraph/services/planning/missing_inputs.py +28 -0
- aethergraph/services/planning/node_planner.py +613 -0
- aethergraph/services/planning/orchestrator.py +112 -0
- aethergraph/services/planning/plan_executor.py +506 -0
- aethergraph/services/planning/plan_types.py +321 -0
- aethergraph/services/planning/planner.py +617 -0
- aethergraph/services/planning/planner_service.py +369 -0
- aethergraph/services/planning/planning_context_builder.py +43 -0
- aethergraph/services/planning/quick_actions.py +29 -0
- aethergraph/services/planning/routers/__init__.py +0 -0
- aethergraph/services/planning/routers/simple_router.py +26 -0
- aethergraph/services/rag/facade.py +0 -3
- aethergraph/services/scope/scope.py +30 -30
- aethergraph/services/scope/scope_factory.py +15 -7
- aethergraph/services/skills/__init__.py +0 -0
- aethergraph/services/skills/skill_registry.py +465 -0
- aethergraph/services/skills/skills.py +220 -0
- aethergraph/services/skills/utils.py +194 -0
- aethergraph/storage/artifacts/artifact_index_jsonl.py +16 -10
- aethergraph/storage/artifacts/artifact_index_sqlite.py +12 -2
- aethergraph/storage/docstore/sqlite_doc_sync.py +1 -1
- aethergraph/storage/memory/event_persist.py +42 -2
- aethergraph/storage/memory/fs_persist.py +32 -2
- aethergraph/storage/search_backend/__init__.py +0 -0
- aethergraph/storage/search_backend/generic_vector_backend.py +230 -0
- aethergraph/storage/search_backend/null_backend.py +34 -0
- aethergraph/storage/search_backend/sqlite_lexical_backend.py +387 -0
- aethergraph/storage/search_backend/utils.py +31 -0
- aethergraph/storage/search_factory.py +75 -0
- aethergraph/storage/vector_index/faiss_index.py +72 -4
- aethergraph/storage/vector_index/sqlite_index.py +521 -52
- aethergraph/storage/vector_index/sqlite_index_vanila.py +311 -0
- aethergraph/storage/vector_index/utils.py +22 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/METADATA +1 -1
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/RECORD +108 -64
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/WHEEL +1 -1
- aethergraph/plugins/agents/default_chat_agent copy.py +0 -90
- aethergraph/server/ui_static/assets/index-BR5GtXcZ.css +0 -1
- aethergraph/server/ui_static/assets/index-CQ0HZZ83.js +0 -400
- aethergraph/services/eventhub/event_hub.py +0 -76
- aethergraph/services/llm/generic_client copy.py +0 -691
- aethergraph/services/prompts/file_store.py +0 -41
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/entry_points.txt +0 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/LICENSE +0 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/licenses/NOTICE +0 -0
- {aethergraph-0.1.0a2.dist-info → aethergraph-0.1.0a4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# aethergraph/services/planning/dependency_index.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from aethergraph.core.graph.action_spec import ActionSpec, IOSlot
|
|
9
|
+
from aethergraph.services.planning.action_catalog import ActionCatalog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DependencyIndex:
|
|
14
|
+
actions: list[ActionSpec]
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_catalog(
|
|
18
|
+
cls,
|
|
19
|
+
catalog: ActionCatalog,
|
|
20
|
+
*,
|
|
21
|
+
flow_ids: list[str] | None = None,
|
|
22
|
+
kinds: Iterable[Literal["graph", "graphfn"]] | None = ("graph", "graphfn"),
|
|
23
|
+
include_global: bool = True,
|
|
24
|
+
) -> DependencyIndex:
|
|
25
|
+
return cls(
|
|
26
|
+
actions=list(
|
|
27
|
+
catalog.list_actions(
|
|
28
|
+
flow_ids=flow_ids,
|
|
29
|
+
kinds=kinds,
|
|
30
|
+
include_global=include_global,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def find_producers(
|
|
36
|
+
self,
|
|
37
|
+
needed: IOSlot,
|
|
38
|
+
*,
|
|
39
|
+
flow_ids: list[str] | None = None,
|
|
40
|
+
) -> list[tuple[ActionSpec, IOSlot]]:
|
|
41
|
+
"""
|
|
42
|
+
Find (action, output_slot) pairs whose outputs are compatible with the given input slot.
|
|
43
|
+
If flow_ids is provided, only consider actions within those flows or global ones (the
|
|
44
|
+
actions list is usually pre-filtered by from_catalog).
|
|
45
|
+
"""
|
|
46
|
+
matches: list[tuple[ActionSpec, IOSlot]] = []
|
|
47
|
+
for act in self.actions:
|
|
48
|
+
if flow_ids is not None: # noqa: SIM102
|
|
49
|
+
# respect pre-filtering convention: allow actions with flow_id in flow_ids or None
|
|
50
|
+
if act.flow_id not in flow_ids and act.flow_id is not None:
|
|
51
|
+
continue
|
|
52
|
+
for out_slot in act.outputs:
|
|
53
|
+
if self._compatible(needed, out_slot):
|
|
54
|
+
matches.append((act, out_slot))
|
|
55
|
+
return matches
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _compatible(inp: IOSlot, out: IOSlot) -> bool:
|
|
59
|
+
if inp.type is None or out.type is None:
|
|
60
|
+
return True
|
|
61
|
+
if inp.type == out.type:
|
|
62
|
+
return True
|
|
63
|
+
if inp.type in {"object", "any"} or out.type in {"object", "any"}: # noqa: SIM103
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# aethergraph/services/planning/flow_validator.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from aethergraph.core.graph.action_spec import ActionSpec, IOSlot
|
|
9
|
+
|
|
10
|
+
from .action_catalog import ActionCatalog
|
|
11
|
+
from .bindings import parse_binding
|
|
12
|
+
from .dependency_index import DependencyIndex
|
|
13
|
+
from .plan_types import CandidatePlan, PlanStep, ValidationIssue, ValidationResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class FlowValidator:
|
|
18
|
+
catalog: ActionCatalog
|
|
19
|
+
dep_index: DependencyIndex | None = None
|
|
20
|
+
|
|
21
|
+
def _action_index(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
flow_ids: list[str] | None = None,
|
|
25
|
+
kinds: Iterable[Literal["graph", "graphfn"]] | None = ("graph", "graphfn"),
|
|
26
|
+
) -> dict[str, ActionSpec]:
|
|
27
|
+
idx: dict[str, ActionSpec] = {}
|
|
28
|
+
for spec in self.catalog.iter_actions(
|
|
29
|
+
flow_ids=flow_ids,
|
|
30
|
+
kinds=kinds,
|
|
31
|
+
include_global=True,
|
|
32
|
+
):
|
|
33
|
+
idx[spec.ref] = spec
|
|
34
|
+
return idx
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _is_strict_type_mismatch(
|
|
38
|
+
expected: str | None,
|
|
39
|
+
actual: str | None,
|
|
40
|
+
) -> bool:
|
|
41
|
+
if not expected or not actual:
|
|
42
|
+
return False
|
|
43
|
+
wildcard = {"any", "object"}
|
|
44
|
+
if expected in wildcard or actual in wildcard:
|
|
45
|
+
return False
|
|
46
|
+
return expected != actual
|
|
47
|
+
|
|
48
|
+
def validate(
|
|
49
|
+
self,
|
|
50
|
+
plan: CandidatePlan,
|
|
51
|
+
*,
|
|
52
|
+
external_inputs: dict[str, IOSlot] | None = None,
|
|
53
|
+
flow_ids: list[str] | None = None,
|
|
54
|
+
) -> ValidationResult:
|
|
55
|
+
issues: list[ValidationIssue] = []
|
|
56
|
+
external_inputs = external_inputs or {}
|
|
57
|
+
missing_user_bindings: dict[str, list[str]] = {}
|
|
58
|
+
|
|
59
|
+
action_index = self._action_index(flow_ids=flow_ids)
|
|
60
|
+
step_index: dict[str, PlanStep] = {step.id: step for step in plan.steps}
|
|
61
|
+
|
|
62
|
+
# 1) unknown actions
|
|
63
|
+
for step in plan.steps:
|
|
64
|
+
if step.action_ref not in action_index:
|
|
65
|
+
issues.append(
|
|
66
|
+
ValidationIssue(
|
|
67
|
+
kind="unknown_action",
|
|
68
|
+
step_id=step.id,
|
|
69
|
+
field=None,
|
|
70
|
+
message=f"Action '{step.action_ref}' is not recognized.",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if any(iss.kind == "unknown_action" for iss in issues):
|
|
75
|
+
# structural error, no missing_user_bindings in this path
|
|
76
|
+
return ValidationResult(
|
|
77
|
+
ok=False,
|
|
78
|
+
issues=issues,
|
|
79
|
+
has_structural_errors=True,
|
|
80
|
+
missing_user_bindings={},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 2) dependency graph
|
|
84
|
+
edges: dict[str, set[str]] = {step.id: set() for step in plan.steps}
|
|
85
|
+
for step in plan.steps:
|
|
86
|
+
for raw in (step.inputs or {}).values():
|
|
87
|
+
binding = parse_binding(raw)
|
|
88
|
+
if binding.kind == "step_output" and binding.source_step_id in step_index:
|
|
89
|
+
edges[step.id].add(binding.source_step_id)
|
|
90
|
+
|
|
91
|
+
# 3) detect cycles
|
|
92
|
+
visiting: set[str] = set()
|
|
93
|
+
visited: set[str] = set()
|
|
94
|
+
has_cycle = False
|
|
95
|
+
|
|
96
|
+
def dfs(node: str) -> None:
|
|
97
|
+
nonlocal has_cycle
|
|
98
|
+
if node in visited or has_cycle:
|
|
99
|
+
return
|
|
100
|
+
if node in visiting:
|
|
101
|
+
has_cycle = True
|
|
102
|
+
return
|
|
103
|
+
visiting.add(node)
|
|
104
|
+
for dep in edges[node]:
|
|
105
|
+
dfs(dep)
|
|
106
|
+
visiting.remove(node)
|
|
107
|
+
visited.add(node)
|
|
108
|
+
|
|
109
|
+
for step_id in edges:
|
|
110
|
+
if step_id not in visited:
|
|
111
|
+
dfs(step_id)
|
|
112
|
+
|
|
113
|
+
if has_cycle:
|
|
114
|
+
issues.append(
|
|
115
|
+
ValidationIssue(
|
|
116
|
+
kind="cycle",
|
|
117
|
+
step_id="",
|
|
118
|
+
field=None,
|
|
119
|
+
message="The plan contains cyclic dependencies among steps.",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return ValidationResult(
|
|
123
|
+
ok=False,
|
|
124
|
+
issues=issues,
|
|
125
|
+
has_structural_errors=True,
|
|
126
|
+
missing_user_bindings={},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# 4) topo order
|
|
130
|
+
in_deg: dict[str, int] = {step_id: 0 for step_id in edges}
|
|
131
|
+
for step_id, deps in edges.items():
|
|
132
|
+
for _ in deps:
|
|
133
|
+
in_deg[step_id] += 1
|
|
134
|
+
|
|
135
|
+
ready = [sid for sid, deg in in_deg.items() if deg == 0]
|
|
136
|
+
topo_order: list[str] = []
|
|
137
|
+
while ready:
|
|
138
|
+
sid = ready.pop()
|
|
139
|
+
topo_order.append(sid)
|
|
140
|
+
for consumer, deps in edges.items():
|
|
141
|
+
if sid in deps:
|
|
142
|
+
in_deg[consumer] -= 1
|
|
143
|
+
if in_deg[consumer] == 0:
|
|
144
|
+
ready.append(consumer)
|
|
145
|
+
|
|
146
|
+
# 5) validate inputs along topo order
|
|
147
|
+
available_outputs: dict[str, IOSlot] = {}
|
|
148
|
+
|
|
149
|
+
for step_id in topo_order:
|
|
150
|
+
step = step_index[step_id]
|
|
151
|
+
spec = action_index[step.action_ref]
|
|
152
|
+
input_by_name = {slot.name: slot for slot in spec.inputs}
|
|
153
|
+
|
|
154
|
+
for name, slot in input_by_name.items():
|
|
155
|
+
raw_value = step.inputs.get(name)
|
|
156
|
+
|
|
157
|
+
if raw_value is None and slot.required:
|
|
158
|
+
details: dict = {}
|
|
159
|
+
if self.dep_index is not None:
|
|
160
|
+
cands = self.dep_index.find_producers(
|
|
161
|
+
slot,
|
|
162
|
+
flow_ids=flow_ids,
|
|
163
|
+
)
|
|
164
|
+
details["candidates"] = [
|
|
165
|
+
{
|
|
166
|
+
"action_ref": a.ref,
|
|
167
|
+
"output": out.name,
|
|
168
|
+
"description": a.description,
|
|
169
|
+
}
|
|
170
|
+
for (a, out) in cands
|
|
171
|
+
]
|
|
172
|
+
issues.append(
|
|
173
|
+
ValidationIssue(
|
|
174
|
+
kind="missing_input",
|
|
175
|
+
step_id=step_id,
|
|
176
|
+
field=name,
|
|
177
|
+
message=(
|
|
178
|
+
f"Required input '{name}' is not provided "
|
|
179
|
+
f"for action '{spec.name}'."
|
|
180
|
+
),
|
|
181
|
+
details=details,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
if raw_value is None:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
binding = parse_binding(raw_value)
|
|
190
|
+
|
|
191
|
+
if binding.kind == "external":
|
|
192
|
+
key = binding.external_key or ""
|
|
193
|
+
ext_slot = external_inputs.get(key)
|
|
194
|
+
if ext_slot is None:
|
|
195
|
+
# treat as “needs user value”, not structural error
|
|
196
|
+
loc = f"{step_id}.{name}"
|
|
197
|
+
missing_user_bindings.setdefault(key, []).append(loc)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# ext_slot can be an IOSlot OR a bare value (when planner passes user_inputs).
|
|
201
|
+
# In the latter case we don't have type info, so we skip strict type checking.
|
|
202
|
+
ext_type = getattr(ext_slot, "type", None)
|
|
203
|
+
if self._is_strict_type_mismatch(slot.type, ext_type):
|
|
204
|
+
issues.append(
|
|
205
|
+
ValidationIssue(
|
|
206
|
+
kind="type_mismatch",
|
|
207
|
+
step_id=step_id,
|
|
208
|
+
field=name,
|
|
209
|
+
message=(
|
|
210
|
+
f"Type mismatch for external input '{key}': "
|
|
211
|
+
f"expected '{slot.type}', got '{ext_slot.type}'."
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
elif binding.kind == "step_output":
|
|
217
|
+
src_id = binding.source_step_id or ""
|
|
218
|
+
out_name = binding.source_output_name or ""
|
|
219
|
+
key = f"{src_id}.{out_name}"
|
|
220
|
+
out_slot = available_outputs.get(key)
|
|
221
|
+
if out_slot is None:
|
|
222
|
+
issues.append(
|
|
223
|
+
ValidationIssue(
|
|
224
|
+
kind="missing_input",
|
|
225
|
+
step_id=step_id,
|
|
226
|
+
field=name,
|
|
227
|
+
message=(
|
|
228
|
+
f"Input '{name}' refers to '{key}', "
|
|
229
|
+
"which is not produced by any previous step."
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
if self._is_strict_type_mismatch(slot.type, out_slot.type):
|
|
235
|
+
issues.append(
|
|
236
|
+
ValidationIssue(
|
|
237
|
+
kind="type_mismatch",
|
|
238
|
+
step_id=step_id,
|
|
239
|
+
field=name,
|
|
240
|
+
message=(
|
|
241
|
+
f"Input '{name}' expects type '{slot.type}' "
|
|
242
|
+
f"but is wired from '{key}' with type '{out_slot.type}'."
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# literals: accepted, no extra checks yet
|
|
248
|
+
|
|
249
|
+
for out in spec.outputs:
|
|
250
|
+
available_outputs[f"{step_id}.{out.name}"] = out
|
|
251
|
+
|
|
252
|
+
# Decide what counts as “structural” (vs. “needs user values”)
|
|
253
|
+
structural_kinds = {"unknown_action", "cycle", "missing_input", "type_mismatch"}
|
|
254
|
+
has_structural_errors = any(iss.kind in structural_kinds for iss in issues)
|
|
255
|
+
|
|
256
|
+
ok = (not has_structural_errors) and (len(missing_user_bindings) == 0)
|
|
257
|
+
|
|
258
|
+
return ValidationResult(
|
|
259
|
+
ok=ok,
|
|
260
|
+
issues=issues,
|
|
261
|
+
has_structural_errors=has_structural_errors,
|
|
262
|
+
missing_user_bindings=missing_user_bindings,
|
|
263
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# aethergraph/services/planning/graph_io_adapter.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aethergraph.core.graph.action_spec import IOSlot, _map_py_type_to_json_type
|
|
7
|
+
from aethergraph.core.graph.task_graph import TaskGraph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def graph_io_to_slots(
|
|
11
|
+
graph: TaskGraph,
|
|
12
|
+
meta: dict[str, Any] | None = None,
|
|
13
|
+
) -> dict[str, list[IOSlot]]:
|
|
14
|
+
"""
|
|
15
|
+
Adapter: TaskGraph.io_signature() + IOSpec + optional registry meta -> IOSlot lists.
|
|
16
|
+
|
|
17
|
+
Priority for types:
|
|
18
|
+
1) registry meta["io_types"]["inputs"/"outputs"] (set by @graphify)
|
|
19
|
+
2) ParamSpec.annotation from IOSpec
|
|
20
|
+
3) None (treated as "any" by planner)
|
|
21
|
+
"""
|
|
22
|
+
sig = graph.io_signature(include_values=False)
|
|
23
|
+
io_spec = getattr(graph.spec, "io", None) # IOSpec if present
|
|
24
|
+
|
|
25
|
+
inputs_info = sig.get("inputs", {}) or {}
|
|
26
|
+
outputs_info = sig.get("outputs", {}) or {}
|
|
27
|
+
|
|
28
|
+
# ---- io_types from registry meta (preferred) ----
|
|
29
|
+
io_types = (meta or {}).get("io_types") or {}
|
|
30
|
+
input_type_map: dict[str, str] = io_types.get("inputs", {}) or {}
|
|
31
|
+
output_type_map: dict[str, str] = io_types.get("outputs", {}) or {}
|
|
32
|
+
|
|
33
|
+
# --- INPUTS ---
|
|
34
|
+
|
|
35
|
+
req_raw = inputs_info.get("required") or []
|
|
36
|
+
opt_raw = inputs_info.get("optional") or {}
|
|
37
|
+
|
|
38
|
+
required_names = list(req_raw.keys()) if isinstance(req_raw, dict) else list(req_raw)
|
|
39
|
+
optional_names = list(opt_raw.keys()) if isinstance(opt_raw, dict) else list(opt_raw)
|
|
40
|
+
|
|
41
|
+
input_slots: list[IOSlot] = []
|
|
42
|
+
|
|
43
|
+
def _param_for(name: str):
|
|
44
|
+
if io_spec is None:
|
|
45
|
+
return None
|
|
46
|
+
if hasattr(io_spec, "required") and name in io_spec.required:
|
|
47
|
+
return io_spec.required[name]
|
|
48
|
+
if hasattr(io_spec, "optional") and name in io_spec.optional:
|
|
49
|
+
return io_spec.optional[name]
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# required inputs
|
|
53
|
+
for name in required_names:
|
|
54
|
+
ps = _param_for(name)
|
|
55
|
+
|
|
56
|
+
# 1) type from meta.io_types if present
|
|
57
|
+
t_from_meta = input_type_map.get(name)
|
|
58
|
+
|
|
59
|
+
# 2) else type from ParamSpec.annotation
|
|
60
|
+
j_type = None
|
|
61
|
+
default = None
|
|
62
|
+
description = None
|
|
63
|
+
required_flag = True
|
|
64
|
+
|
|
65
|
+
if ps is not None:
|
|
66
|
+
anno = getattr(ps, "annotation", None)
|
|
67
|
+
default = getattr(ps, "default", None)
|
|
68
|
+
required_flag = getattr(ps, "required", True)
|
|
69
|
+
description = getattr(ps, "description", None)
|
|
70
|
+
if t_from_meta is None and anno is not None:
|
|
71
|
+
j_type = _map_py_type_to_json_type(anno)
|
|
72
|
+
|
|
73
|
+
# final type choice
|
|
74
|
+
final_type = t_from_meta or j_type
|
|
75
|
+
|
|
76
|
+
input_slots.append(
|
|
77
|
+
IOSlot(
|
|
78
|
+
name=name,
|
|
79
|
+
type=final_type,
|
|
80
|
+
required=required_flag,
|
|
81
|
+
default=None if required_flag else default,
|
|
82
|
+
description=description,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# optional inputs
|
|
87
|
+
for name in optional_names:
|
|
88
|
+
ps = _param_for(name)
|
|
89
|
+
|
|
90
|
+
t_from_meta = input_type_map.get(name)
|
|
91
|
+
|
|
92
|
+
j_type = None
|
|
93
|
+
default = None
|
|
94
|
+
description = None
|
|
95
|
+
|
|
96
|
+
if ps is not None:
|
|
97
|
+
anno = getattr(ps, "annotation", None)
|
|
98
|
+
default = getattr(ps, "default", None)
|
|
99
|
+
description = getattr(ps, "description", None)
|
|
100
|
+
if t_from_meta is None and anno is not None:
|
|
101
|
+
j_type = _map_py_type_to_json_type(anno)
|
|
102
|
+
|
|
103
|
+
final_type = t_from_meta or j_type
|
|
104
|
+
|
|
105
|
+
input_slots.append(
|
|
106
|
+
IOSlot(
|
|
107
|
+
name=name,
|
|
108
|
+
type=final_type,
|
|
109
|
+
required=False,
|
|
110
|
+
default=default,
|
|
111
|
+
description=description,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# --- OUTPUTS ---
|
|
116
|
+
|
|
117
|
+
output_keys = outputs_info.get("keys") or []
|
|
118
|
+
output_slots: list[IOSlot] = []
|
|
119
|
+
|
|
120
|
+
def _output_param_for(name: str):
|
|
121
|
+
if io_spec is None or not hasattr(io_spec, "outputs"):
|
|
122
|
+
return None
|
|
123
|
+
return io_spec.outputs.get(name)
|
|
124
|
+
|
|
125
|
+
for name in output_keys:
|
|
126
|
+
ps = _output_param_for(name)
|
|
127
|
+
|
|
128
|
+
t_from_meta = output_type_map.get(name)
|
|
129
|
+
|
|
130
|
+
j_type = None
|
|
131
|
+
description = None
|
|
132
|
+
|
|
133
|
+
if ps is not None:
|
|
134
|
+
anno = getattr(ps, "annotation", None)
|
|
135
|
+
description = getattr(ps, "description", None)
|
|
136
|
+
if t_from_meta is None and anno is not None:
|
|
137
|
+
j_type = _map_py_type_to_json_type(anno)
|
|
138
|
+
|
|
139
|
+
final_type = t_from_meta or j_type
|
|
140
|
+
|
|
141
|
+
output_slots.append(
|
|
142
|
+
IOSlot(
|
|
143
|
+
name=name,
|
|
144
|
+
type=final_type,
|
|
145
|
+
required=True, # outputs are logically “present”
|
|
146
|
+
description=description,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return {"inputs": input_slots, "outputs": output_slots}
|