flowspec2 1.0.0__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.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""address@1 — geocode + optional confirmation.
|
|
2
|
+
|
|
3
|
+
Collects a free-text address, geocodes/validates it via the injectable
|
|
4
|
+
``geocode`` tool, optionally confirms it (Yes/No), and exposes the ``address``
|
|
5
|
+
slot (a dict carrying ``kind`` so the public-square gate can read ``address.kind``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Callable, Mapping
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from langgraph.graph import END
|
|
16
|
+
|
|
17
|
+
from ..domains import parse_affirmation
|
|
18
|
+
from ..interactive import options_from_domain
|
|
19
|
+
from ..models import CORRECTION_REQUESTED_INTERNAL_KEY, AgentResponse, ServiceState
|
|
20
|
+
from ..nodes import (
|
|
21
|
+
NEXT,
|
|
22
|
+
FlowContext,
|
|
23
|
+
NodeDesc,
|
|
24
|
+
_handle_node_error,
|
|
25
|
+
clear_cascade,
|
|
26
|
+
mark_flow_finished,
|
|
27
|
+
)
|
|
28
|
+
from ..observability import log_event
|
|
29
|
+
from . import SubflowBuild
|
|
30
|
+
|
|
31
|
+
_ADDRESS_COMPLETED_KEY = "address_completed"
|
|
32
|
+
_ADDRESS_DEFAULTED_KEY = "address_defaulted"
|
|
33
|
+
_ADDRESS_SKIPPED_KEY = "address_skipped"
|
|
34
|
+
_ADDRESS_EXHAUSTION_MODES = frozenset({"reask", "skip", "default", "handoff", "END"})
|
|
35
|
+
_AddressCompletionOutcome = Literal["confirmed", "skipped", "defaulted"]
|
|
36
|
+
_AUX = [
|
|
37
|
+
"address_confirmed",
|
|
38
|
+
"address_needs_confirmation",
|
|
39
|
+
"address_attempts",
|
|
40
|
+
_ADDRESS_COMPLETED_KEY,
|
|
41
|
+
_ADDRESS_DEFAULTED_KEY,
|
|
42
|
+
_ADDRESS_SKIPPED_KEY,
|
|
43
|
+
]
|
|
44
|
+
_ADDRESS_NOT_RESOLVED_ERROR = "the address could not be located"
|
|
45
|
+
_ADDRESS_REQUIRED_ERROR = "the address is required"
|
|
46
|
+
_ADDRESS_CONFIRMATION_ERROR = "the response could not be interpreted as yes or no"
|
|
47
|
+
_ADDRESS_EXHAUSTED_ERROR = "maximum attempts reached; let us try again"
|
|
48
|
+
_ADDRESS_HANDOFF_DESCRIPTION = "I will transfer you to a support agent."
|
|
49
|
+
_ADDRESS_END_DESCRIPTION = "I could not validate the address. Try again later."
|
|
50
|
+
_ADDRESS_PROMPT = "What is the complete address (street, number, and district)?"
|
|
51
|
+
_OPTIONAL_ADDRESS_PROMPT = f"{_ADDRESS_PROMPT} You may skip it if you prefer not to provide it."
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _address_payload_schema(required: bool) -> dict[str, Any]:
|
|
56
|
+
"""Describe a required address or an optional address with an explicit null skip."""
|
|
57
|
+
|
|
58
|
+
address_type: str | list[str] = "string" if required else ["string", "null"]
|
|
59
|
+
address_description = "Complete address: street, number, and district."
|
|
60
|
+
if not required:
|
|
61
|
+
address_description += " Use null when the user chooses not to provide it."
|
|
62
|
+
return {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"address": {
|
|
66
|
+
"type": address_type,
|
|
67
|
+
"description": address_description,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"required": ["address"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _address_is_complete(state: ServiceState) -> bool:
|
|
75
|
+
return bool(state.data.get(_ADDRESS_COMPLETED_KEY) or state.data.get("address_confirmed"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _complete_address(state: ServiceState, *, outcome: _AddressCompletionOutcome) -> None:
|
|
79
|
+
state.data[_ADDRESS_COMPLETED_KEY] = True
|
|
80
|
+
state.data.pop("address_attempts", None)
|
|
81
|
+
if outcome == "confirmed":
|
|
82
|
+
state.data["address_confirmed"] = True
|
|
83
|
+
elif outcome == "skipped":
|
|
84
|
+
state.data.pop("address", None)
|
|
85
|
+
state.data[_ADDRESS_SKIPPED_KEY] = True
|
|
86
|
+
elif outcome == "defaulted":
|
|
87
|
+
state.data["address"] = None
|
|
88
|
+
state.data[_ADDRESS_DEFAULTED_KEY] = True
|
|
89
|
+
state.agent_response = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AddressSubflow:
|
|
93
|
+
name = "address"
|
|
94
|
+
major = 1
|
|
95
|
+
|
|
96
|
+
def build(self, ctx: FlowContext, with_cfg: dict[str, Any]) -> SubflowBuild:
|
|
97
|
+
required = bool(with_cfg.get("required", ctx.config.get("address_required", False)))
|
|
98
|
+
needs_confirmation = bool(with_cfg.get("needs_confirmation", True))
|
|
99
|
+
max_attempts = int(with_cfg.get("max_attempts", ctx.max_attempts))
|
|
100
|
+
on_exhaust = str(with_cfg.get("on_exhaust", "reask"))
|
|
101
|
+
if max_attempts < 1:
|
|
102
|
+
raise ValueError("address@1 max_attempts must be positive")
|
|
103
|
+
if on_exhaust not in _ADDRESS_EXHAUSTION_MODES:
|
|
104
|
+
raise ValueError(f"address@1 has unsupported on_exhaust mode: {on_exhaust!r}")
|
|
105
|
+
payload_schema = _address_payload_schema(required)
|
|
106
|
+
prompt = _ADDRESS_PROMPT if required else _OPTIONAL_ADDRESS_PROMPT
|
|
107
|
+
|
|
108
|
+
ctx.slot_aux["address"] = _AUX
|
|
109
|
+
ctx.dependents.setdefault("address", set()) # populated by the compiler's requires pass
|
|
110
|
+
|
|
111
|
+
def ask(error_message: str | None = None) -> AgentResponse:
|
|
112
|
+
return AgentResponse(
|
|
113
|
+
description=prompt,
|
|
114
|
+
payload_schema=payload_schema,
|
|
115
|
+
error_message=error_message,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def confirmation_response(
|
|
119
|
+
state: ServiceState,
|
|
120
|
+
error_message: str | None = None,
|
|
121
|
+
) -> AgentResponse:
|
|
122
|
+
resolved_address = state.data.get("address") or {}
|
|
123
|
+
body = (
|
|
124
|
+
"Do you confirm the address: "
|
|
125
|
+
f"{resolved_address.get('street', '')}, {resolved_address.get('district', '')}?"
|
|
126
|
+
)
|
|
127
|
+
interactive_specification = (
|
|
128
|
+
options_from_domain(
|
|
129
|
+
{
|
|
130
|
+
"kind": "buttons",
|
|
131
|
+
"field": "confirmation",
|
|
132
|
+
"from_domain": "YesNo",
|
|
133
|
+
"body": body,
|
|
134
|
+
},
|
|
135
|
+
ctx.domains,
|
|
136
|
+
state=state,
|
|
137
|
+
config=ctx.config,
|
|
138
|
+
)
|
|
139
|
+
if "YesNo" in ctx.domains
|
|
140
|
+
else None
|
|
141
|
+
)
|
|
142
|
+
return AgentResponse(
|
|
143
|
+
description=body,
|
|
144
|
+
error_message=error_message,
|
|
145
|
+
interactive=interactive_specification,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def exhaust(
|
|
149
|
+
state: ServiceState,
|
|
150
|
+
*,
|
|
151
|
+
reask_response: AgentResponse | None = None,
|
|
152
|
+
) -> ServiceState:
|
|
153
|
+
if on_exhaust == "skip":
|
|
154
|
+
_complete_address(state, outcome="skipped")
|
|
155
|
+
elif on_exhaust == "default":
|
|
156
|
+
_complete_address(state, outcome="defaulted")
|
|
157
|
+
elif on_exhaust == "handoff":
|
|
158
|
+
state.agent_response = AgentResponse(description=_ADDRESS_HANDOFF_DESCRIPTION)
|
|
159
|
+
elif on_exhaust == "END":
|
|
160
|
+
mark_flow_finished(state, reset_next=True)
|
|
161
|
+
state.status = "completed"
|
|
162
|
+
log_id = log_event(
|
|
163
|
+
logger,
|
|
164
|
+
logging.WARNING,
|
|
165
|
+
"Flow stopped after address collection attempts were exhausted",
|
|
166
|
+
operation="collect_address",
|
|
167
|
+
log_id_generator=ctx.log_id_generator,
|
|
168
|
+
context={"flow": state.service_name, "slot": "address"},
|
|
169
|
+
)
|
|
170
|
+
state.agent_response = AgentResponse(
|
|
171
|
+
description=_ADDRESS_END_DESCRIPTION,
|
|
172
|
+
log_id=log_id,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
state.data.pop("address_attempts", None)
|
|
176
|
+
state.agent_response = reask_response or ask(_ADDRESS_EXHAUSTED_ERROR)
|
|
177
|
+
return state
|
|
178
|
+
|
|
179
|
+
def register_failed_attempt(
|
|
180
|
+
state: ServiceState,
|
|
181
|
+
error_message: str,
|
|
182
|
+
*,
|
|
183
|
+
response_factory: Callable[[str | None], AgentResponse] = ask,
|
|
184
|
+
) -> ServiceState:
|
|
185
|
+
address_attempts = int(state.data.get("address_attempts", 0)) + 1
|
|
186
|
+
state.data["address_attempts"] = address_attempts
|
|
187
|
+
if address_attempts >= max_attempts:
|
|
188
|
+
reask_response = (
|
|
189
|
+
response_factory(_ADDRESS_EXHAUSTED_ERROR) if on_exhaust == "reask" else None
|
|
190
|
+
)
|
|
191
|
+
return exhaust(
|
|
192
|
+
state,
|
|
193
|
+
reask_response=reask_response,
|
|
194
|
+
)
|
|
195
|
+
state.agent_response = response_factory(error_message)
|
|
196
|
+
return state
|
|
197
|
+
|
|
198
|
+
# ── collect_address ──────────────────────────────────────────────
|
|
199
|
+
async def collect(state: ServiceState) -> ServiceState:
|
|
200
|
+
state.status = "progress"
|
|
201
|
+
if state.internal.get(CORRECTION_REQUESTED_INTERNAL_KEY) == "address":
|
|
202
|
+
# clear address + its aux + every requires-dependent (sports_court,
|
|
203
|
+
# reference_point) so a corrected address never submits stale
|
|
204
|
+
# downstream data.
|
|
205
|
+
clear_cascade(state, "address", ctx)
|
|
206
|
+
state.internal.pop(CORRECTION_REQUESTED_INTERNAL_KEY, None)
|
|
207
|
+
if _address_is_complete(state):
|
|
208
|
+
state.agent_response = None
|
|
209
|
+
return state
|
|
210
|
+
if "address" in state.payload:
|
|
211
|
+
address_input = state.payload["address"]
|
|
212
|
+
if address_input is None:
|
|
213
|
+
if required:
|
|
214
|
+
return register_failed_attempt(state, _ADDRESS_REQUIRED_ERROR)
|
|
215
|
+
_complete_address(state, outcome="skipped")
|
|
216
|
+
return state
|
|
217
|
+
resolved_address: Mapping[str, Any] | None = None
|
|
218
|
+
try:
|
|
219
|
+
geocode_result = await ctx.tools.call("geocode", address=str(address_input))
|
|
220
|
+
if not isinstance(geocode_result, Mapping):
|
|
221
|
+
raise TypeError("geocode returned a non-object result")
|
|
222
|
+
if geocode_result.get("status") == "ok":
|
|
223
|
+
resolved_address = geocode_result.get("address")
|
|
224
|
+
if not isinstance(resolved_address, Mapping):
|
|
225
|
+
raise ValueError("geocode success did not contain an address object")
|
|
226
|
+
street_name = resolved_address.get("street")
|
|
227
|
+
if not isinstance(street_name, str) or not street_name.strip():
|
|
228
|
+
raise ValueError("geocode success did not contain a meaningful address")
|
|
229
|
+
except Exception as exc: # noqa: BLE001
|
|
230
|
+
error_state = _handle_node_error(
|
|
231
|
+
state,
|
|
232
|
+
exc,
|
|
233
|
+
ctx=ctx,
|
|
234
|
+
operation="collect_address",
|
|
235
|
+
)
|
|
236
|
+
assert error_state.agent_response is not None
|
|
237
|
+
error_state.agent_response.description = (
|
|
238
|
+
"I could not look up the address. Try again."
|
|
239
|
+
)
|
|
240
|
+
error_state.agent_response.payload_schema = payload_schema
|
|
241
|
+
return error_state
|
|
242
|
+
|
|
243
|
+
if geocode_result.get("status") == "ok":
|
|
244
|
+
assert isinstance(resolved_address, Mapping)
|
|
245
|
+
state.data["address"] = copy.deepcopy(dict(resolved_address))
|
|
246
|
+
state.data.pop("address_attempts", None)
|
|
247
|
+
if needs_confirmation and geocode_result.get("needs_confirmation"):
|
|
248
|
+
state.data["address_needs_confirmation"] = True
|
|
249
|
+
state.agent_response = None
|
|
250
|
+
return state
|
|
251
|
+
_complete_address(state, outcome="confirmed")
|
|
252
|
+
return state
|
|
253
|
+
return register_failed_attempt(
|
|
254
|
+
state,
|
|
255
|
+
str(geocode_result.get("error") or _ADDRESS_NOT_RESOLVED_ERROR),
|
|
256
|
+
)
|
|
257
|
+
state.agent_response = ask()
|
|
258
|
+
return state
|
|
259
|
+
|
|
260
|
+
def collect_router(state: ServiceState) -> str:
|
|
261
|
+
if _address_is_complete(state):
|
|
262
|
+
return "address_done"
|
|
263
|
+
if state.data.get("address") and state.data.get("address_needs_confirmation"):
|
|
264
|
+
return "confirm_address"
|
|
265
|
+
return END # asking
|
|
266
|
+
|
|
267
|
+
# ── confirm_address ──────────────────────────────────────────────
|
|
268
|
+
async def confirm(state: ServiceState) -> ServiceState:
|
|
269
|
+
if _address_is_complete(state):
|
|
270
|
+
state.agent_response = None
|
|
271
|
+
return state
|
|
272
|
+
confirmation_field = (
|
|
273
|
+
"address_confirmation"
|
|
274
|
+
if "address_confirmation" in state.payload
|
|
275
|
+
else "confirmation"
|
|
276
|
+
)
|
|
277
|
+
if confirmation_field in state.payload:
|
|
278
|
+
confirmation = parse_affirmation(state.payload[confirmation_field])
|
|
279
|
+
if confirmation is True:
|
|
280
|
+
_complete_address(state, outcome="confirmed")
|
|
281
|
+
return state
|
|
282
|
+
if confirmation is False:
|
|
283
|
+
for key in ["address", *_AUX]:
|
|
284
|
+
state.data.pop(key, None)
|
|
285
|
+
state.agent_response = None
|
|
286
|
+
return state
|
|
287
|
+
return register_failed_attempt(
|
|
288
|
+
state,
|
|
289
|
+
_ADDRESS_CONFIRMATION_ERROR,
|
|
290
|
+
response_factory=lambda error_message: confirmation_response(
|
|
291
|
+
state, error_message
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
state.agent_response = confirmation_response(state)
|
|
295
|
+
return state
|
|
296
|
+
|
|
297
|
+
def confirm_router(state: ServiceState) -> str:
|
|
298
|
+
if _address_is_complete(state):
|
|
299
|
+
return "address_done"
|
|
300
|
+
if not state.data.get("address"):
|
|
301
|
+
return "collect_address" # rejected → re-collect
|
|
302
|
+
return END # asking
|
|
303
|
+
|
|
304
|
+
# ── exit node ────────────────────────────────────────────────────
|
|
305
|
+
async def done(state: ServiceState) -> ServiceState:
|
|
306
|
+
state.agent_response = None
|
|
307
|
+
return state
|
|
308
|
+
|
|
309
|
+
descriptors = [
|
|
310
|
+
NodeDesc(
|
|
311
|
+
"collect_address",
|
|
312
|
+
collect,
|
|
313
|
+
collect_router,
|
|
314
|
+
targets=["confirm_address", "address_done"],
|
|
315
|
+
),
|
|
316
|
+
NodeDesc(
|
|
317
|
+
"confirm_address",
|
|
318
|
+
confirm,
|
|
319
|
+
confirm_router,
|
|
320
|
+
targets=["collect_address", "address_done"],
|
|
321
|
+
),
|
|
322
|
+
NodeDesc("address_done", done, lambda s: NEXT),
|
|
323
|
+
]
|
|
324
|
+
return SubflowBuild(
|
|
325
|
+
descriptors=descriptors,
|
|
326
|
+
entry_id="collect_address",
|
|
327
|
+
node_for_slot={"address": "collect_address"},
|
|
328
|
+
)
|