agentplane-core 0.0.1__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.
- agentplane_core/__init__.py +158 -0
- agentplane_core/card_json.py +34 -0
- agentplane_core/definition.py +411 -0
- agentplane_core/interfaces.py +69 -0
- agentplane_core/registry.py +259 -0
- agentplane_core/resources.py +105 -0
- agentplane_core/schema_export.py +31 -0
- agentplane_core/types.py +54 -0
- agentplane_core/validation.py +476 -0
- agentplane_core-0.0.1.dist-info/METADATA +21 -0
- agentplane_core-0.0.1.dist-info/RECORD +12 -0
- agentplane_core-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""agentplane-core: shared contract — Definition schema, registry models, interfaces."""
|
|
2
|
+
|
|
3
|
+
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
|
|
4
|
+
|
|
5
|
+
from agentplane_core.card_json import agent_card_from_dict, agent_card_to_json_dict
|
|
6
|
+
from agentplane_core.definition import (
|
|
7
|
+
DEPRECATED_NODE_VERSIONS,
|
|
8
|
+
NODE_CATALOG,
|
|
9
|
+
SCHEMA_VERSION,
|
|
10
|
+
Edge,
|
|
11
|
+
EndNode,
|
|
12
|
+
EndNodeConfig,
|
|
13
|
+
ExposeConfig,
|
|
14
|
+
FlowDefinition,
|
|
15
|
+
Layout,
|
|
16
|
+
LayoutPosition,
|
|
17
|
+
LlmCallNode,
|
|
18
|
+
LlmCallNodeConfig,
|
|
19
|
+
McpToolNode,
|
|
20
|
+
McpToolNodeConfig,
|
|
21
|
+
Node,
|
|
22
|
+
RetrievalNode,
|
|
23
|
+
RetrievalNodeConfig,
|
|
24
|
+
RouterInputType,
|
|
25
|
+
RouterNode,
|
|
26
|
+
RouterNodeConfig,
|
|
27
|
+
RouterRule,
|
|
28
|
+
StartNode,
|
|
29
|
+
StartNodeConfig,
|
|
30
|
+
TemplateNode,
|
|
31
|
+
TemplateNodeConfig,
|
|
32
|
+
input_ports,
|
|
33
|
+
output_ports,
|
|
34
|
+
ports_compatible,
|
|
35
|
+
prompt_variables,
|
|
36
|
+
single_required_string_input,
|
|
37
|
+
)
|
|
38
|
+
from agentplane_core.interfaces import ScoredHit, SearchBackend, SecretsProvider, VectorStore
|
|
39
|
+
from agentplane_core.registry import (
|
|
40
|
+
AuthMode,
|
|
41
|
+
Capabilities,
|
|
42
|
+
Card,
|
|
43
|
+
DefinitionInfo,
|
|
44
|
+
DefinitionStatus,
|
|
45
|
+
DeploymentInfo,
|
|
46
|
+
EntryKind,
|
|
47
|
+
HealthStatus,
|
|
48
|
+
Page,
|
|
49
|
+
RegistryEntry,
|
|
50
|
+
RegistryEntryCreate,
|
|
51
|
+
RegistryEntryPatch,
|
|
52
|
+
SearchQuery,
|
|
53
|
+
Severity,
|
|
54
|
+
ToolCard,
|
|
55
|
+
ToolCardTool,
|
|
56
|
+
ValidationIssue,
|
|
57
|
+
ValidationResult,
|
|
58
|
+
serialize_card,
|
|
59
|
+
)
|
|
60
|
+
from agentplane_core.resources import (
|
|
61
|
+
SECRET_PLACEHOLDER,
|
|
62
|
+
VECTOR_DB_KINDS,
|
|
63
|
+
EmbeddingConfig,
|
|
64
|
+
McpServerResource,
|
|
65
|
+
ModelProviderResource,
|
|
66
|
+
Resource,
|
|
67
|
+
ResourceKind,
|
|
68
|
+
VectorDBResource,
|
|
69
|
+
)
|
|
70
|
+
from agentplane_core.types import (
|
|
71
|
+
Document,
|
|
72
|
+
JsonObject,
|
|
73
|
+
JsonSchema,
|
|
74
|
+
PortType,
|
|
75
|
+
render_documents,
|
|
76
|
+
split_port_ref,
|
|
77
|
+
)
|
|
78
|
+
from agentplane_core.validation import ErrorCode, validate_structure
|
|
79
|
+
|
|
80
|
+
__version__ = "0.0.1"
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"DEPRECATED_NODE_VERSIONS",
|
|
84
|
+
"NODE_CATALOG",
|
|
85
|
+
"SCHEMA_VERSION",
|
|
86
|
+
"SECRET_PLACEHOLDER",
|
|
87
|
+
"VECTOR_DB_KINDS",
|
|
88
|
+
"AgentCapabilities",
|
|
89
|
+
"AgentCard",
|
|
90
|
+
"AgentInterface",
|
|
91
|
+
"AgentSkill",
|
|
92
|
+
"AuthMode",
|
|
93
|
+
"Capabilities",
|
|
94
|
+
"Card",
|
|
95
|
+
"DefinitionInfo",
|
|
96
|
+
"DefinitionStatus",
|
|
97
|
+
"DeploymentInfo",
|
|
98
|
+
"Document",
|
|
99
|
+
"Edge",
|
|
100
|
+
"EmbeddingConfig",
|
|
101
|
+
"EndNode",
|
|
102
|
+
"EndNodeConfig",
|
|
103
|
+
"EntryKind",
|
|
104
|
+
"ErrorCode",
|
|
105
|
+
"ExposeConfig",
|
|
106
|
+
"FlowDefinition",
|
|
107
|
+
"HealthStatus",
|
|
108
|
+
"JsonObject",
|
|
109
|
+
"JsonSchema",
|
|
110
|
+
"Layout",
|
|
111
|
+
"LayoutPosition",
|
|
112
|
+
"LlmCallNode",
|
|
113
|
+
"LlmCallNodeConfig",
|
|
114
|
+
"McpServerResource",
|
|
115
|
+
"McpToolNode",
|
|
116
|
+
"McpToolNodeConfig",
|
|
117
|
+
"ModelProviderResource",
|
|
118
|
+
"Node",
|
|
119
|
+
"Page",
|
|
120
|
+
"PortType",
|
|
121
|
+
"RegistryEntry",
|
|
122
|
+
"RegistryEntryCreate",
|
|
123
|
+
"RegistryEntryPatch",
|
|
124
|
+
"Resource",
|
|
125
|
+
"ResourceKind",
|
|
126
|
+
"RetrievalNode",
|
|
127
|
+
"RetrievalNodeConfig",
|
|
128
|
+
"RouterInputType",
|
|
129
|
+
"RouterNode",
|
|
130
|
+
"RouterNodeConfig",
|
|
131
|
+
"RouterRule",
|
|
132
|
+
"ScoredHit",
|
|
133
|
+
"SearchBackend",
|
|
134
|
+
"SearchQuery",
|
|
135
|
+
"SecretsProvider",
|
|
136
|
+
"Severity",
|
|
137
|
+
"StartNode",
|
|
138
|
+
"StartNodeConfig",
|
|
139
|
+
"TemplateNode",
|
|
140
|
+
"TemplateNodeConfig",
|
|
141
|
+
"ToolCard",
|
|
142
|
+
"ToolCardTool",
|
|
143
|
+
"ValidationIssue",
|
|
144
|
+
"ValidationResult",
|
|
145
|
+
"VectorDBResource",
|
|
146
|
+
"VectorStore",
|
|
147
|
+
"agent_card_from_dict",
|
|
148
|
+
"agent_card_to_json_dict",
|
|
149
|
+
"input_ports",
|
|
150
|
+
"output_ports",
|
|
151
|
+
"ports_compatible",
|
|
152
|
+
"prompt_variables",
|
|
153
|
+
"render_documents",
|
|
154
|
+
"serialize_card",
|
|
155
|
+
"single_required_string_input",
|
|
156
|
+
"split_port_ref",
|
|
157
|
+
"validate_structure",
|
|
158
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""AgentCard <-> A2A JSON wire format.
|
|
2
|
+
|
|
3
|
+
The official ``a2a.types.AgentCard`` is a protobuf message; its JSON wire
|
|
4
|
+
format (camelCase, per A2A v1.0) is produced/consumed with the protobuf JSON
|
|
5
|
+
mapping. ``google.protobuf`` ships with ``a2a-sdk`` — no extra dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
from a2a.types import AgentCard
|
|
13
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
14
|
+
|
|
15
|
+
from agentplane_core.types import JsonObject
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def agent_card_to_json_dict(card: AgentCard) -> JsonObject:
|
|
19
|
+
"""Serialize an AgentCard to its A2A JSON object form."""
|
|
20
|
+
return cast(JsonObject, MessageToDict(card))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def agent_card_from_dict(data: JsonObject) -> AgentCard:
|
|
24
|
+
"""Parse an A2A JSON card into the official protobuf AgentCard.
|
|
25
|
+
|
|
26
|
+
Unknown fields (e.g. v0.3 compatibility fields some servers still emit)
|
|
27
|
+
are ignored.
|
|
28
|
+
"""
|
|
29
|
+
card = AgentCard()
|
|
30
|
+
ParseDict(data, card, ignore_unknown_fields=True)
|
|
31
|
+
return card
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["agent_card_from_dict", "agent_card_to_json_dict"]
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""The Definition schema — the platform's single artifact type (SPEC §3.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Annotated, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
9
|
+
|
|
10
|
+
from agentplane_core.types import (
|
|
11
|
+
BranchName,
|
|
12
|
+
JsonObject,
|
|
13
|
+
JsonSchema,
|
|
14
|
+
NodeId,
|
|
15
|
+
PortType,
|
|
16
|
+
Slug,
|
|
17
|
+
ToolName,
|
|
18
|
+
split_port_ref,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
SCHEMA_VERSION = 1
|
|
22
|
+
|
|
23
|
+
# (type, version) pairs the platform knows how to execute. Unknown pairs fail
|
|
24
|
+
# validation with E002. Append-only per node-type version.
|
|
25
|
+
NODE_CATALOG: frozenset[tuple[str, int]] = frozenset(
|
|
26
|
+
{
|
|
27
|
+
("start", 1),
|
|
28
|
+
("end", 1),
|
|
29
|
+
("llm_call", 1),
|
|
30
|
+
("mcp_tool", 1),
|
|
31
|
+
("retrieval", 1),
|
|
32
|
+
# v1.1 additions
|
|
33
|
+
("router", 1),
|
|
34
|
+
("template", 1),
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# (type, version) pairs that still run but warn with W001 on validation.
|
|
39
|
+
DEPRECATED_NODE_VERSIONS: frozenset[tuple[str, int]] = frozenset()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _StrictModel(BaseModel):
|
|
43
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ExposeConfig(_StrictModel):
|
|
47
|
+
"""How the runtime serves a flow: as an A2A agent or as an MCP server."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
50
|
+
|
|
51
|
+
kind: Literal["a2a", "mcp"]
|
|
52
|
+
tool_name: ToolName | None = None
|
|
53
|
+
tool_description: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StartNodeConfig(_StrictModel):
|
|
57
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
58
|
+
|
|
59
|
+
input_schema: JsonSchema
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EndNodeConfig(_StrictModel):
|
|
63
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
64
|
+
|
|
65
|
+
output_from: str = Field(
|
|
66
|
+
default="",
|
|
67
|
+
description=(
|
|
68
|
+
"Reference 'node_id.port' the flow output is read from. Empty means: "
|
|
69
|
+
"take the value wired into the `input` port — required for branched "
|
|
70
|
+
"flows where different runs feed `end` from different nodes."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LlmCallNodeConfig(_StrictModel):
|
|
76
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
77
|
+
|
|
78
|
+
resource: Slug
|
|
79
|
+
model: str = ""
|
|
80
|
+
prompt: str
|
|
81
|
+
system_prompt: str = ""
|
|
82
|
+
structured_output: JsonSchema | None = None
|
|
83
|
+
stream: bool = False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class McpToolNodeConfig(_StrictModel):
|
|
87
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
88
|
+
|
|
89
|
+
resource: Slug | None = None
|
|
90
|
+
url: str | None = None
|
|
91
|
+
tool: str
|
|
92
|
+
args: dict[str, str] = Field(
|
|
93
|
+
default_factory=dict, description="input port name -> tool argument name"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RetrievalNodeConfig(_StrictModel):
|
|
98
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
99
|
+
|
|
100
|
+
resource: Slug
|
|
101
|
+
collection: str
|
|
102
|
+
top_k: int = Field(default=4, ge=1, le=100)
|
|
103
|
+
filter: JsonObject | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
RouterInputType = Literal["text", "json", "documents"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RouterRule(_StrictModel):
|
|
110
|
+
"""One branch condition, evaluated in order; first match wins."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
113
|
+
|
|
114
|
+
when: Literal["not_empty", "empty"]
|
|
115
|
+
branch: BranchName
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RouterNodeConfig(_StrictModel):
|
|
119
|
+
"""Conditional branching (v1.1): routes the input value to one branch.
|
|
120
|
+
|
|
121
|
+
Every rule branch plus the default branch becomes a typed output port
|
|
122
|
+
that passes the input value through; only the chosen branch's downstream
|
|
123
|
+
nodes execute.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
127
|
+
|
|
128
|
+
input_type: RouterInputType = "text"
|
|
129
|
+
rules: list[RouterRule] = Field(min_length=1)
|
|
130
|
+
default_branch: BranchName = "otherwise"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TemplateNodeConfig(_StrictModel):
|
|
134
|
+
"""Static/interpolated text (v1.1): `{vars}` become input ports.
|
|
135
|
+
|
|
136
|
+
The `trigger` input port exists on every template so a router branch can
|
|
137
|
+
activate it even when the text needs no variables (fallback messages).
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
141
|
+
|
|
142
|
+
text: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _NodeBase(_StrictModel):
|
|
146
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
147
|
+
|
|
148
|
+
id: NodeId
|
|
149
|
+
version: int = Field(ge=1)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class StartNode(_NodeBase):
|
|
153
|
+
type: Literal["start"]
|
|
154
|
+
config: StartNodeConfig
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class EndNode(_NodeBase):
|
|
158
|
+
type: Literal["end"]
|
|
159
|
+
config: EndNodeConfig
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class LlmCallNode(_NodeBase):
|
|
163
|
+
type: Literal["llm_call"]
|
|
164
|
+
config: LlmCallNodeConfig
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class McpToolNode(_NodeBase):
|
|
168
|
+
type: Literal["mcp_tool"]
|
|
169
|
+
config: McpToolNodeConfig
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class RetrievalNode(_NodeBase):
|
|
173
|
+
type: Literal["retrieval"]
|
|
174
|
+
config: RetrievalNodeConfig
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class RouterNode(_NodeBase):
|
|
178
|
+
type: Literal["router"]
|
|
179
|
+
config: RouterNodeConfig
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TemplateNode(_NodeBase):
|
|
183
|
+
type: Literal["template"]
|
|
184
|
+
config: TemplateNodeConfig
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
Node = Annotated[
|
|
188
|
+
StartNode | EndNode | LlmCallNode | McpToolNode | RetrievalNode | RouterNode | TemplateNode,
|
|
189
|
+
Field(discriminator="type"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Edge(_StrictModel):
|
|
194
|
+
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
|
195
|
+
|
|
196
|
+
from_: str = Field(alias="from", serialization_alias="from")
|
|
197
|
+
to: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class LayoutPosition(BaseModel):
|
|
201
|
+
"""Canvas position — builder-owned, ignored by the runtime."""
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
204
|
+
|
|
205
|
+
x: float = 0
|
|
206
|
+
y: float = 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Layout(BaseModel):
|
|
210
|
+
"""Canvas layout block — builder-owned, ignored by the runtime."""
|
|
211
|
+
|
|
212
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
213
|
+
|
|
214
|
+
nodes: dict[str, LayoutPosition] = Field(default_factory=dict)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class FlowDefinition(_StrictModel):
|
|
218
|
+
"""Versioned, serializable description of a flow (SPEC §3.1).
|
|
219
|
+
|
|
220
|
+
Serialization is deterministic: nodes are sorted by id, edges by
|
|
221
|
+
(from, to), keys stay in model order — ``parse(serialize(d)) == d``.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
225
|
+
|
|
226
|
+
schema_version: int
|
|
227
|
+
name: Slug
|
|
228
|
+
display_name: str = ""
|
|
229
|
+
description: str = ""
|
|
230
|
+
tags: list[str] = Field(default_factory=list)
|
|
231
|
+
expose: ExposeConfig
|
|
232
|
+
nodes: list[Node]
|
|
233
|
+
edges: list[Edge] = Field(default_factory=list)
|
|
234
|
+
layout: Layout | None = None
|
|
235
|
+
|
|
236
|
+
@model_validator(mode="before")
|
|
237
|
+
@classmethod
|
|
238
|
+
def _canonical_order(cls, data: object) -> object:
|
|
239
|
+
"""Sort nodes by id and edges by (from, to) for deterministic serialization."""
|
|
240
|
+
if not isinstance(data, dict):
|
|
241
|
+
return data
|
|
242
|
+
|
|
243
|
+
def _node_key(item: object) -> str:
|
|
244
|
+
if isinstance(item, dict):
|
|
245
|
+
node_id = item.get("id", "")
|
|
246
|
+
return node_id if isinstance(node_id, str) else ""
|
|
247
|
+
return getattr(item, "id", "")
|
|
248
|
+
|
|
249
|
+
def _edge_key(item: object) -> tuple[str, str]:
|
|
250
|
+
if isinstance(item, dict):
|
|
251
|
+
from_ = item.get("from", item.get("from_", ""))
|
|
252
|
+
to = item.get("to", "")
|
|
253
|
+
return (from_ if isinstance(from_, str) else "", to if isinstance(to, str) else "")
|
|
254
|
+
return (getattr(item, "from_", ""), getattr(item, "to", ""))
|
|
255
|
+
|
|
256
|
+
nodes = data.get("nodes")
|
|
257
|
+
if isinstance(nodes, list):
|
|
258
|
+
data = {**data, "nodes": sorted(nodes, key=_node_key)}
|
|
259
|
+
edges = data.get("edges")
|
|
260
|
+
if isinstance(edges, list):
|
|
261
|
+
data = {**data, "edges": sorted(edges, key=_edge_key)}
|
|
262
|
+
return data
|
|
263
|
+
|
|
264
|
+
def node_map(self) -> dict[str, Node]:
|
|
265
|
+
"""Nodes keyed by id (last one wins on duplicates; E040 catches those)."""
|
|
266
|
+
return {node.id: node for node in self.nodes}
|
|
267
|
+
|
|
268
|
+
def canonical_dict(self) -> JsonObject:
|
|
269
|
+
"""Deterministic JSON-mode dump used for export and hashing."""
|
|
270
|
+
dumped: JsonObject = self.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
271
|
+
return dumped
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# `{vars}` in prompts become named input ports of that node. Double braces
|
|
275
|
+
# (`{{literal}}`) are not ports.
|
|
276
|
+
_PROMPT_VAR_RE = re.compile(r"(?<!\{)\{([a-z_][a-z0-9_]*)\}(?!\})")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def prompt_variables(template: str) -> list[str]:
|
|
280
|
+
"""Extract `{var}` input ports from a prompt template, in first-seen order."""
|
|
281
|
+
seen: dict[str, None] = {}
|
|
282
|
+
for match in _PROMPT_VAR_RE.finditer(template):
|
|
283
|
+
seen.setdefault(match.group(1))
|
|
284
|
+
return list(seen)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _schema_property_port_type(prop_schema: object) -> PortType:
|
|
288
|
+
if isinstance(prop_schema, dict) and prop_schema.get("type") == "string":
|
|
289
|
+
return "text"
|
|
290
|
+
return "json"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def output_ports(node: Node) -> dict[str, PortType]: # noqa: PLR0911 - one return per node type
|
|
294
|
+
"""Typed output ports a node exposes."""
|
|
295
|
+
match node:
|
|
296
|
+
case StartNode():
|
|
297
|
+
props = node.config.input_schema.get("properties")
|
|
298
|
+
if not isinstance(props, dict):
|
|
299
|
+
return {}
|
|
300
|
+
return {name: _schema_property_port_type(schema) for name, schema in props.items()}
|
|
301
|
+
case EndNode():
|
|
302
|
+
return {}
|
|
303
|
+
case LlmCallNode():
|
|
304
|
+
ports: dict[str, PortType] = {"text": "text"}
|
|
305
|
+
if node.config.structured_output is not None:
|
|
306
|
+
ports["json"] = "json"
|
|
307
|
+
return ports
|
|
308
|
+
case McpToolNode():
|
|
309
|
+
return {"result": "json"}
|
|
310
|
+
case RetrievalNode():
|
|
311
|
+
return {"documents": "documents"}
|
|
312
|
+
case RouterNode():
|
|
313
|
+
branches: dict[str, PortType] = {
|
|
314
|
+
rule.branch: node.config.input_type for rule in node.config.rules
|
|
315
|
+
}
|
|
316
|
+
branches[node.config.default_branch] = node.config.input_type
|
|
317
|
+
return branches
|
|
318
|
+
case TemplateNode():
|
|
319
|
+
return {"text": "text"}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def input_ports(node: Node) -> dict[str, PortType]: # noqa: PLR0911 - one return per node type
|
|
323
|
+
"""Typed input ports a node accepts."""
|
|
324
|
+
match node:
|
|
325
|
+
case StartNode():
|
|
326
|
+
return {}
|
|
327
|
+
case EndNode():
|
|
328
|
+
return {"input": "text"}
|
|
329
|
+
case LlmCallNode():
|
|
330
|
+
names = prompt_variables(node.config.prompt) + prompt_variables(
|
|
331
|
+
node.config.system_prompt
|
|
332
|
+
)
|
|
333
|
+
return dict.fromkeys(names, "text")
|
|
334
|
+
case McpToolNode():
|
|
335
|
+
return dict.fromkeys(node.config.args, "json")
|
|
336
|
+
case RetrievalNode():
|
|
337
|
+
return {"query": "text"}
|
|
338
|
+
case RouterNode():
|
|
339
|
+
return {"input": node.config.input_type}
|
|
340
|
+
case TemplateNode():
|
|
341
|
+
template_ports: dict[str, PortType] = {"trigger": "text"}
|
|
342
|
+
for name in prompt_variables(node.config.text):
|
|
343
|
+
template_ports[name] = "text"
|
|
344
|
+
return template_ports
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def ports_compatible(src: PortType, dst: PortType) -> bool:
|
|
348
|
+
"""Port type compatibility matrix (SPEC §3.1).
|
|
349
|
+
|
|
350
|
+
Identical types always connect. ``documents -> text`` is rendered
|
|
351
|
+
implicitly (concatenated chunks with source headers); ``json -> text``
|
|
352
|
+
is stringified; ``text -> json`` is a plain JSON string value.
|
|
353
|
+
"""
|
|
354
|
+
if src == dst:
|
|
355
|
+
return True
|
|
356
|
+
return (src, dst) in {("documents", "text"), ("json", "text"), ("text", "json")}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def single_required_string_input(defn: FlowDefinition) -> str | None:
|
|
360
|
+
"""Name of the single required string property of ``start.input_schema``, if any.
|
|
361
|
+
|
|
362
|
+
Used for A2A input binding: plain message text binds to this property
|
|
363
|
+
(SPEC §6.4); otherwise the message must be a JSON object.
|
|
364
|
+
"""
|
|
365
|
+
start = next((n for n in defn.nodes if isinstance(n, StartNode)), None)
|
|
366
|
+
if start is None:
|
|
367
|
+
return None
|
|
368
|
+
schema = start.config.input_schema
|
|
369
|
+
required = schema.get("required")
|
|
370
|
+
props = schema.get("properties")
|
|
371
|
+
if not isinstance(required, list) or len(required) != 1 or not isinstance(props, dict):
|
|
372
|
+
return None
|
|
373
|
+
name = required[0]
|
|
374
|
+
if not isinstance(name, str):
|
|
375
|
+
return None
|
|
376
|
+
return name if _schema_property_port_type(props.get(name)) == "text" else None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
__all__ = [
|
|
380
|
+
"DEPRECATED_NODE_VERSIONS",
|
|
381
|
+
"NODE_CATALOG",
|
|
382
|
+
"SCHEMA_VERSION",
|
|
383
|
+
"Edge",
|
|
384
|
+
"EndNode",
|
|
385
|
+
"EndNodeConfig",
|
|
386
|
+
"ExposeConfig",
|
|
387
|
+
"FlowDefinition",
|
|
388
|
+
"Layout",
|
|
389
|
+
"LayoutPosition",
|
|
390
|
+
"LlmCallNode",
|
|
391
|
+
"LlmCallNodeConfig",
|
|
392
|
+
"McpToolNode",
|
|
393
|
+
"McpToolNodeConfig",
|
|
394
|
+
"Node",
|
|
395
|
+
"RetrievalNode",
|
|
396
|
+
"RetrievalNodeConfig",
|
|
397
|
+
"RouterInputType",
|
|
398
|
+
"RouterNode",
|
|
399
|
+
"RouterNodeConfig",
|
|
400
|
+
"RouterRule",
|
|
401
|
+
"StartNode",
|
|
402
|
+
"StartNodeConfig",
|
|
403
|
+
"TemplateNode",
|
|
404
|
+
"TemplateNodeConfig",
|
|
405
|
+
"input_ports",
|
|
406
|
+
"output_ports",
|
|
407
|
+
"ports_compatible",
|
|
408
|
+
"prompt_variables",
|
|
409
|
+
"single_required_string_input",
|
|
410
|
+
"split_port_ref",
|
|
411
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Abstract interfaces implemented by the services (SPEC §3.3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue
|
|
10
|
+
|
|
11
|
+
from agentplane_core.registry import Page, RegistryEntry, SearchQuery
|
|
12
|
+
from agentplane_core.types import JsonObject
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ScoredHit(BaseModel):
|
|
16
|
+
"""One vector search hit."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(frozen=True)
|
|
19
|
+
|
|
20
|
+
key: str
|
|
21
|
+
score: float
|
|
22
|
+
payload: JsonObject = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SecretsProvider(ABC):
|
|
26
|
+
"""Stores credentials referenced by name; values never leave the provider.
|
|
27
|
+
|
|
28
|
+
Default implementation in the runtime: Fernet-encrypted DB column.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def put(self, ref: str, value: str) -> None: ...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def get(self, ref: str) -> str: ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def delete(self, ref: str) -> None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VectorStore(ABC):
|
|
42
|
+
"""Vector index used by registry ``[semantic]`` and runtime retrieval."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def upsert(self, key: str, vector: list[float], payload: JsonObject) -> None: ...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def search(
|
|
49
|
+
self,
|
|
50
|
+
vector: list[float],
|
|
51
|
+
top_k: int,
|
|
52
|
+
filter: Mapping[str, JsonValue] | None = None,
|
|
53
|
+
) -> list[ScoredHit]: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SearchBackend(ABC):
|
|
57
|
+
"""Registry search abstraction."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
async def index(self, entry: RegistryEntry) -> None: ...
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def remove(self, entry_id: UUID) -> None: ...
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def search(self, q: SearchQuery) -> Page: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["ScoredHit", "SearchBackend", "SecretsProvider", "VectorStore"]
|