openai-sdk-helpers 0.6.0__py3-none-any.whl → 0.6.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.
- openai_sdk_helpers/agent/base.py +13 -5
- openai_sdk_helpers/agent/classifier.py +673 -93
- openai_sdk_helpers/prompt/classifier.jinja +9 -4
- openai_sdk_helpers/settings.py +65 -0
- openai_sdk_helpers/structure/base.py +79 -55
- openai_sdk_helpers/structure/classification.py +191 -43
- openai_sdk_helpers/structure/plan/enum.py +4 -0
- {openai_sdk_helpers-0.6.0.dist-info → openai_sdk_helpers-0.6.1.dist-info}/METADATA +12 -1
- {openai_sdk_helpers-0.6.0.dist-info → openai_sdk_helpers-0.6.1.dist-info}/RECORD +12 -12
- {openai_sdk_helpers-0.6.0.dist-info → openai_sdk_helpers-0.6.1.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.6.0.dist-info → openai_sdk_helpers-0.6.1.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.6.0.dist-info → openai_sdk_helpers-0.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
You are a taxonomy classification assistant.
|
|
2
2
|
|
|
3
3
|
Instructions:
|
|
4
|
-
- Review the text and select
|
|
4
|
+
- Review the text and select all matching taxonomy nodes from the list.
|
|
5
|
+
- Populate selected_nodes as a list of taxonomy node ids for multi-class matches.
|
|
6
|
+
- Use selected_node when a single best match is appropriate.
|
|
7
|
+
- Provide a confidence score between 0 and 1 for the selections; higher means more certain.
|
|
8
|
+
- Use only taxonomy identifiers from the candidate list for any selections.
|
|
9
|
+
- Use the stop_reason enum values only: "continue", "stop", "no_match", "max_depth", "no_children".
|
|
5
10
|
- If a child level should be explored, set stop_reason to "continue".
|
|
6
|
-
- If no appropriate node exists, set stop_reason to "no_match" and leave
|
|
11
|
+
- If no appropriate node exists, set stop_reason to "no_match" and leave selections empty.
|
|
7
12
|
- If you are confident this is the final level, set stop_reason to "stop".
|
|
8
13
|
- Provide a concise rationale in one or two sentences.
|
|
9
14
|
|
|
@@ -12,7 +17,7 @@ Current depth: {{ depth }}
|
|
|
12
17
|
Previous path:
|
|
13
18
|
{% if path %}
|
|
14
19
|
{% for step in path %}
|
|
15
|
-
- {{ step.
|
|
20
|
+
- {{ step.selected_node }} (confidence={{ step.confidence }}, stop_reason={{ step.stop_reason }})
|
|
16
21
|
{% endfor %}
|
|
17
22
|
{% else %}
|
|
18
23
|
- None
|
|
@@ -20,7 +25,7 @@ Previous path:
|
|
|
20
25
|
|
|
21
26
|
Candidate taxonomy nodes:
|
|
22
27
|
{% for node in taxonomy_nodes %}
|
|
23
|
-
-
|
|
28
|
+
- identifier: {{ node.identifier }}
|
|
24
29
|
label: {{ node.label }}
|
|
25
30
|
description: {{ node.description or "None" }}
|
|
26
31
|
{% endfor %}
|
openai_sdk_helpers/settings.py
CHANGED
|
@@ -48,6 +48,8 @@ class OpenAISettings(BaseModel):
|
|
|
48
48
|
-------
|
|
49
49
|
from_env(dotenv_path, **overrides)
|
|
50
50
|
Build settings from environment variables and optional overrides.
|
|
51
|
+
from_secrets(secrets, **overrides)
|
|
52
|
+
Build settings from a secrets mapping and optional overrides.
|
|
51
53
|
client_kwargs()
|
|
52
54
|
Return keyword arguments for ``OpenAI`` initialization.
|
|
53
55
|
create_client()
|
|
@@ -190,6 +192,69 @@ class OpenAISettings(BaseModel):
|
|
|
190
192
|
|
|
191
193
|
return settings
|
|
192
194
|
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_secrets(
|
|
197
|
+
cls,
|
|
198
|
+
secrets: Mapping[str, Any] | None = None,
|
|
199
|
+
**overrides: Any,
|
|
200
|
+
) -> OpenAISettings:
|
|
201
|
+
"""Load settings from a secrets mapping and optional overrides.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
secrets : Mapping[str, Any] or None, optional
|
|
206
|
+
Mapping of secret values keyed by environment variable names.
|
|
207
|
+
Defaults to environment variables.
|
|
208
|
+
overrides : Any
|
|
209
|
+
Keyword overrides applied on top of secret values.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
OpenAISettings
|
|
214
|
+
Settings instance populated from secret values and overrides.
|
|
215
|
+
|
|
216
|
+
Raises
|
|
217
|
+
------
|
|
218
|
+
ValueError
|
|
219
|
+
If OPENAI_API_KEY is not found in the secrets mapping.
|
|
220
|
+
"""
|
|
221
|
+
secret_values: Mapping[str, Any] = secrets or os.environ
|
|
222
|
+
|
|
223
|
+
def first_non_none(*candidates: Any) -> Any:
|
|
224
|
+
for candidate in candidates:
|
|
225
|
+
if candidate is not None:
|
|
226
|
+
return candidate
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def resolve_value(override_key: str, secret_key: str) -> Any:
|
|
230
|
+
return first_non_none(
|
|
231
|
+
overrides.get(override_key),
|
|
232
|
+
secret_values.get(secret_key),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
timeout_raw = resolve_value("timeout", "OPENAI_TIMEOUT")
|
|
236
|
+
max_retries_raw = resolve_value("max_retries", "OPENAI_MAX_RETRIES")
|
|
237
|
+
|
|
238
|
+
values: dict[str, Any] = {
|
|
239
|
+
"api_key": resolve_value("api_key", "OPENAI_API_KEY"),
|
|
240
|
+
"org_id": resolve_value("org_id", "OPENAI_ORG_ID"),
|
|
241
|
+
"project_id": resolve_value("project_id", "OPENAI_PROJECT_ID"),
|
|
242
|
+
"base_url": resolve_value("base_url", "OPENAI_BASE_URL"),
|
|
243
|
+
"default_model": resolve_value("default_model", "OPENAI_MODEL"),
|
|
244
|
+
"timeout": coerce_optional_float(timeout_raw),
|
|
245
|
+
"max_retries": coerce_optional_int(max_retries_raw),
|
|
246
|
+
"extra_client_kwargs": coerce_dict(overrides.get("extra_client_kwargs")),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
settings = cls(**values)
|
|
250
|
+
if not settings.api_key:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
"OPENAI_API_KEY is required to configure the OpenAI client"
|
|
253
|
+
" and was not found in secrets."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return settings
|
|
257
|
+
|
|
193
258
|
def client_kwargs(self) -> dict[str, Any]:
|
|
194
259
|
"""Return keyword arguments for constructing an OpenAI client.
|
|
195
260
|
|
|
@@ -134,9 +134,21 @@ def _ensure_items_have_schema(target: Any) -> None:
|
|
|
134
134
|
|
|
135
135
|
def _ensure_schema_has_type(schema: dict[str, Any]) -> None:
|
|
136
136
|
"""Ensure a schema dictionary includes a type entry when possible."""
|
|
137
|
+
any_of = schema.get("anyOf")
|
|
138
|
+
if isinstance(any_of, list):
|
|
139
|
+
for entry in any_of:
|
|
140
|
+
if isinstance(entry, dict):
|
|
141
|
+
_ensure_schema_has_type(entry)
|
|
142
|
+
properties = schema.get("properties")
|
|
143
|
+
if isinstance(properties, dict):
|
|
144
|
+
for value in properties.values():
|
|
145
|
+
if isinstance(value, dict):
|
|
146
|
+
_ensure_schema_has_type(value)
|
|
147
|
+
items = schema.get("items")
|
|
148
|
+
if isinstance(items, dict):
|
|
149
|
+
_ensure_schema_has_type(items)
|
|
137
150
|
if "type" in schema or "$ref" in schema:
|
|
138
151
|
return
|
|
139
|
-
any_of = schema.get("anyOf")
|
|
140
152
|
if isinstance(any_of, list):
|
|
141
153
|
inferred_types: set[str] = set()
|
|
142
154
|
for entry in any_of:
|
|
@@ -162,6 +174,68 @@ def _ensure_schema_has_type(schema: dict[str, Any]) -> None:
|
|
|
162
174
|
schema.update(_build_any_value_schema())
|
|
163
175
|
|
|
164
176
|
|
|
177
|
+
def _hydrate_ref_types(schema: dict[str, Any]) -> None:
|
|
178
|
+
"""Attach explicit types to $ref nodes when available.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
schema : dict[str, Any]
|
|
183
|
+
Schema dictionary to hydrate in place.
|
|
184
|
+
"""
|
|
185
|
+
definitions = schema.get("$defs") or schema.get("definitions") or {}
|
|
186
|
+
if not isinstance(definitions, dict):
|
|
187
|
+
definitions = {}
|
|
188
|
+
|
|
189
|
+
def _infer_enum_type(values: list[Any]) -> list[str] | str | None:
|
|
190
|
+
type_map = {
|
|
191
|
+
str: "string",
|
|
192
|
+
int: "integer",
|
|
193
|
+
float: "number",
|
|
194
|
+
bool: "boolean",
|
|
195
|
+
type(None): "null",
|
|
196
|
+
}
|
|
197
|
+
inferred: set[str] = set()
|
|
198
|
+
for value in values:
|
|
199
|
+
inferred_type = type_map.get(type(value))
|
|
200
|
+
if inferred_type is not None:
|
|
201
|
+
inferred.add(inferred_type)
|
|
202
|
+
if not inferred:
|
|
203
|
+
return None
|
|
204
|
+
if len(inferred) == 1:
|
|
205
|
+
return next(iter(inferred))
|
|
206
|
+
return sorted(inferred)
|
|
207
|
+
|
|
208
|
+
def _resolve_ref_type(ref: str) -> list[str] | str | None:
|
|
209
|
+
prefixes = ("#/$defs/", "#/definitions/")
|
|
210
|
+
if not ref.startswith(prefixes):
|
|
211
|
+
return None
|
|
212
|
+
key = ref.split("/", maxsplit=2)[-1]
|
|
213
|
+
definition = definitions.get(key)
|
|
214
|
+
if not isinstance(definition, dict):
|
|
215
|
+
return None
|
|
216
|
+
ref_type = definition.get("type")
|
|
217
|
+
if isinstance(ref_type, (str, list)):
|
|
218
|
+
return ref_type
|
|
219
|
+
enum_values = definition.get("enum")
|
|
220
|
+
if isinstance(enum_values, list):
|
|
221
|
+
return _infer_enum_type(enum_values)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def _walk(node: Any) -> None:
|
|
225
|
+
if isinstance(node, dict):
|
|
226
|
+
if "$ref" in node and "type" not in node:
|
|
227
|
+
ref_type = _resolve_ref_type(node["$ref"])
|
|
228
|
+
if ref_type is not None:
|
|
229
|
+
node["type"] = ref_type
|
|
230
|
+
for value in node.values():
|
|
231
|
+
_walk(value)
|
|
232
|
+
elif isinstance(node, list):
|
|
233
|
+
for item in node:
|
|
234
|
+
_walk(item)
|
|
235
|
+
|
|
236
|
+
_walk(schema)
|
|
237
|
+
|
|
238
|
+
|
|
165
239
|
class StructureBase(BaseModelJSONSerializable):
|
|
166
240
|
"""Base class for structured output models with schema generation.
|
|
167
241
|
|
|
@@ -471,7 +545,7 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
471
545
|
if isinstance(obj, dict):
|
|
472
546
|
if "$ref" in obj:
|
|
473
547
|
for key in list(obj.keys()):
|
|
474
|
-
if key
|
|
548
|
+
if key not in {"$ref", "type"}:
|
|
475
549
|
obj.pop(key, None)
|
|
476
550
|
for v in obj.values():
|
|
477
551
|
clean_refs(v)
|
|
@@ -482,60 +556,10 @@ class StructureBase(BaseModelJSONSerializable):
|
|
|
482
556
|
|
|
483
557
|
cleaned_schema = cast(dict[str, Any], clean_refs(schema))
|
|
484
558
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
root: dict[str, Any],
|
|
488
|
-
seen: set[str],
|
|
489
|
-
) -> dict[str, Any] | None:
|
|
490
|
-
if not ref.startswith("#/"):
|
|
491
|
-
return None
|
|
492
|
-
if ref in seen:
|
|
493
|
-
return None
|
|
494
|
-
seen.add(ref)
|
|
495
|
-
|
|
496
|
-
current: Any = root
|
|
497
|
-
for part in ref.lstrip("#/").split("/"):
|
|
498
|
-
part = part.replace("~1", "/").replace("~0", "~")
|
|
499
|
-
if isinstance(current, dict) and part in current:
|
|
500
|
-
current = current[part]
|
|
501
|
-
else:
|
|
502
|
-
seen.discard(ref)
|
|
503
|
-
return None
|
|
504
|
-
if isinstance(current, dict):
|
|
505
|
-
resolved = cast(dict[str, Any], json.loads(json.dumps(current)))
|
|
506
|
-
else:
|
|
507
|
-
resolved = None
|
|
508
|
-
seen.discard(ref)
|
|
509
|
-
return resolved
|
|
510
|
-
|
|
511
|
-
def _inline_anyof_refs(obj: Any, root: dict[str, Any], seen: set[str]) -> Any:
|
|
512
|
-
if isinstance(obj, dict):
|
|
513
|
-
updated: dict[str, Any] = {}
|
|
514
|
-
for key, value in obj.items():
|
|
515
|
-
if key == "anyOf" and isinstance(value, list):
|
|
516
|
-
updated_items = []
|
|
517
|
-
for item in value:
|
|
518
|
-
if (
|
|
519
|
-
isinstance(item, dict)
|
|
520
|
-
and "$ref" in item
|
|
521
|
-
and "type" not in item
|
|
522
|
-
):
|
|
523
|
-
resolved = _resolve_ref(item["$ref"], root, seen)
|
|
524
|
-
if resolved is not None:
|
|
525
|
-
item = resolved
|
|
526
|
-
updated_items.append(_inline_anyof_refs(item, root, seen))
|
|
527
|
-
updated[key] = updated_items
|
|
528
|
-
else:
|
|
529
|
-
updated[key] = _inline_anyof_refs(value, root, seen)
|
|
530
|
-
return updated
|
|
531
|
-
if isinstance(obj, list):
|
|
532
|
-
return [_inline_anyof_refs(item, root, seen) for item in obj]
|
|
533
|
-
return obj
|
|
534
|
-
|
|
535
|
-
cleaned_schema = cast(
|
|
536
|
-
dict[str, Any], _inline_anyof_refs(cleaned_schema, schema, set())
|
|
537
|
-
)
|
|
559
|
+
cleaned_schema = cast(dict[str, Any], cleaned_schema)
|
|
560
|
+
_hydrate_ref_types(cleaned_schema)
|
|
538
561
|
_ensure_items_have_schema(cleaned_schema)
|
|
562
|
+
_ensure_schema_has_type(cleaned_schema)
|
|
539
563
|
|
|
540
564
|
nullable_fields = {
|
|
541
565
|
name
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Any, Iterable, Optional
|
|
6
|
+
from typing import Any, Iterable, Optional, cast
|
|
7
7
|
|
|
8
8
|
from .base import StructureBase, spec_field
|
|
9
9
|
|
|
@@ -13,11 +13,9 @@ class TaxonomyNode(StructureBase):
|
|
|
13
13
|
|
|
14
14
|
Attributes
|
|
15
15
|
----------
|
|
16
|
-
id : str
|
|
17
|
-
Unique identifier for the taxonomy node.
|
|
18
16
|
label : str
|
|
19
17
|
Human-readable label for the taxonomy node.
|
|
20
|
-
description : str
|
|
18
|
+
description : str | None
|
|
21
19
|
Optional description of the node.
|
|
22
20
|
children : list[TaxonomyNode]
|
|
23
21
|
Child nodes in the taxonomy.
|
|
@@ -30,15 +28,14 @@ class TaxonomyNode(StructureBase):
|
|
|
30
28
|
Return the computed path for the node.
|
|
31
29
|
is_leaf
|
|
32
30
|
Return True when the taxonomy node has no children.
|
|
33
|
-
|
|
34
|
-
Return the child node matching the provided
|
|
31
|
+
child_by_path(path)
|
|
32
|
+
Return the child node matching the provided path.
|
|
35
33
|
"""
|
|
36
34
|
|
|
37
|
-
id: str = spec_field("id", description="Unique identifier for the taxonomy.")
|
|
38
35
|
label: str = spec_field(
|
|
39
36
|
"label", description="Human-readable label for the taxonomy node."
|
|
40
37
|
)
|
|
41
|
-
description:
|
|
38
|
+
description: str | None = spec_field(
|
|
42
39
|
"description",
|
|
43
40
|
description="Optional description of the taxonomy node.",
|
|
44
41
|
default=None,
|
|
@@ -88,22 +85,53 @@ class TaxonomyNode(StructureBase):
|
|
|
88
85
|
"""
|
|
89
86
|
return self.build_path()
|
|
90
87
|
|
|
91
|
-
def
|
|
92
|
-
|
|
88
|
+
def child_by_path(
|
|
89
|
+
self, path: Iterable[str] | str | None
|
|
90
|
+
) -> Optional["TaxonomyNode"]:
|
|
91
|
+
"""Return the child node matching the provided path.
|
|
93
92
|
|
|
94
93
|
Parameters
|
|
95
94
|
----------
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
path : Iterable[str] or str or None
|
|
96
|
+
Path segments or a delimited path string to locate.
|
|
98
97
|
|
|
99
98
|
Returns
|
|
100
99
|
-------
|
|
101
100
|
TaxonomyNode or None
|
|
102
101
|
Matching child node, if found.
|
|
103
102
|
"""
|
|
104
|
-
if
|
|
103
|
+
if path is None:
|
|
105
104
|
return None
|
|
106
|
-
|
|
105
|
+
if isinstance(path, str):
|
|
106
|
+
path_segments = _split_path_identifier(path)
|
|
107
|
+
else:
|
|
108
|
+
path_segments = list(path)
|
|
109
|
+
last_segment = path_segments[-1] if path_segments else None
|
|
110
|
+
if not last_segment:
|
|
111
|
+
return None
|
|
112
|
+
return next(
|
|
113
|
+
(child for child in self.children if child.label == last_segment),
|
|
114
|
+
None,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _split_path_identifier(path: str) -> list[str]:
|
|
119
|
+
"""Split a path identifier into label segments.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
path : str
|
|
124
|
+
Path identifier to split.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
list[str]
|
|
129
|
+
Label segments extracted from the path identifier.
|
|
130
|
+
"""
|
|
131
|
+
delimiter = " > "
|
|
132
|
+
escape_token = "\\>"
|
|
133
|
+
segments = path.split(delimiter) if path else []
|
|
134
|
+
return [segment.replace(escape_token, delimiter) for segment in segments]
|
|
107
135
|
|
|
108
136
|
|
|
109
137
|
class ClassificationStopReason(str, Enum):
|
|
@@ -139,14 +167,14 @@ class ClassificationStopReason(str, Enum):
|
|
|
139
167
|
|
|
140
168
|
|
|
141
169
|
class ClassificationStep(StructureBase):
|
|
142
|
-
"""Represent a
|
|
170
|
+
"""Represent a classification step constrained to taxonomy node enums.
|
|
143
171
|
|
|
144
172
|
Attributes
|
|
145
173
|
----------
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
selected_node : Enum or None
|
|
175
|
+
Enum value of the selected taxonomy node.
|
|
176
|
+
selected_nodes : list[Enum] or None
|
|
177
|
+
Enum values of selected taxonomy nodes for multi-class classification.
|
|
150
178
|
confidence : float or None
|
|
151
179
|
Confidence score between 0 and 1.
|
|
152
180
|
stop_reason : ClassificationStopReason
|
|
@@ -156,18 +184,34 @@ class ClassificationStep(StructureBase):
|
|
|
156
184
|
|
|
157
185
|
Methods
|
|
158
186
|
-------
|
|
187
|
+
build_for_enum(enum_cls)
|
|
188
|
+
Build a ClassificationStep subclass with enum-constrained selections.
|
|
159
189
|
as_summary()
|
|
160
190
|
Return a dictionary summary of the classification step.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
--------
|
|
194
|
+
Create a multi-class step and summarize the selections:
|
|
195
|
+
|
|
196
|
+
>>> NodeEnum = Enum("NodeEnum", {"BILLING": "billing"})
|
|
197
|
+
>>> StepEnum = ClassificationStep.build_for_enum(NodeEnum)
|
|
198
|
+
>>> step = StepEnum(
|
|
199
|
+
... selected_nodes=[NodeEnum.BILLING],
|
|
200
|
+
... confidence=0.82,
|
|
201
|
+
... stop_reason=ClassificationStopReason.STOP,
|
|
202
|
+
... )
|
|
203
|
+
>>> step.as_summary()["selected_nodes"]
|
|
204
|
+
[<NodeEnum.BILLING: 'billing'>]
|
|
161
205
|
"""
|
|
162
206
|
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
description="
|
|
207
|
+
selected_node: Enum | None = spec_field(
|
|
208
|
+
"selected_node",
|
|
209
|
+
description="Path identifier of the selected taxonomy node.",
|
|
166
210
|
default=None,
|
|
167
211
|
)
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
description="
|
|
212
|
+
selected_nodes: list[Enum] | None = spec_field(
|
|
213
|
+
"selected_nodes",
|
|
214
|
+
description="Path identifiers of selected taxonomy nodes.",
|
|
171
215
|
default=None,
|
|
172
216
|
)
|
|
173
217
|
confidence: Optional[float] = spec_field(
|
|
@@ -179,6 +223,7 @@ class ClassificationStep(StructureBase):
|
|
|
179
223
|
"stop_reason",
|
|
180
224
|
description="Reason for stopping or continuing traversal.",
|
|
181
225
|
default=ClassificationStopReason.STOP,
|
|
226
|
+
allow_null=False,
|
|
182
227
|
)
|
|
183
228
|
rationale: Optional[str] = spec_field(
|
|
184
229
|
"rationale",
|
|
@@ -186,6 +231,38 @@ class ClassificationStep(StructureBase):
|
|
|
186
231
|
default=None,
|
|
187
232
|
)
|
|
188
233
|
|
|
234
|
+
@classmethod
|
|
235
|
+
def build_for_enum(cls, enum_cls: type[Enum]) -> type["ClassificationStep"]:
|
|
236
|
+
"""Build a ClassificationStep subclass with enum-constrained fields.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
enum_cls : type[Enum]
|
|
241
|
+
Enum type to use for node selections.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
type[ClassificationStep]
|
|
246
|
+
Specialized ClassificationStep class bound to the enum.
|
|
247
|
+
"""
|
|
248
|
+
namespace: dict[str, Any] = {
|
|
249
|
+
"__annotations__": {
|
|
250
|
+
"selected_node": enum_cls | None,
|
|
251
|
+
"selected_nodes": list[enum_cls] | None,
|
|
252
|
+
},
|
|
253
|
+
"selected_node": spec_field(
|
|
254
|
+
"selected_node",
|
|
255
|
+
description="Path identifier of the selected taxonomy node.",
|
|
256
|
+
default=None,
|
|
257
|
+
),
|
|
258
|
+
"selected_nodes": spec_field(
|
|
259
|
+
"selected_nodes",
|
|
260
|
+
description="Path identifiers of selected taxonomy nodes.",
|
|
261
|
+
default=None,
|
|
262
|
+
),
|
|
263
|
+
}
|
|
264
|
+
return cast(type["ClassificationStep"], type("BoundStep", (cls,), namespace))
|
|
265
|
+
|
|
189
266
|
def as_summary(self) -> dict[str, Any]:
|
|
190
267
|
"""Return a dictionary summary of the classification step.
|
|
191
268
|
|
|
@@ -193,47 +270,93 @@ class ClassificationStep(StructureBase):
|
|
|
193
270
|
-------
|
|
194
271
|
dict[str, Any]
|
|
195
272
|
Summary data for logging or inspection.
|
|
273
|
+
|
|
274
|
+
Examples
|
|
275
|
+
--------
|
|
276
|
+
>>> NodeEnum = Enum("NodeEnum", {"ROOT": "root"})
|
|
277
|
+
>>> StepEnum = ClassificationStep.build_for_enum(NodeEnum)
|
|
278
|
+
>>> step = StepEnum(selected_node=NodeEnum.ROOT)
|
|
279
|
+
>>> step.as_summary()["selected_node"]
|
|
280
|
+
<NodeEnum.ROOT: 'root'>
|
|
196
281
|
"""
|
|
282
|
+
selected_node = _normalize_enum_value(self.selected_node)
|
|
283
|
+
selected_nodes = [
|
|
284
|
+
_normalize_enum_value(item) for item in self.selected_nodes or []
|
|
285
|
+
]
|
|
197
286
|
return {
|
|
198
|
-
"
|
|
199
|
-
"
|
|
287
|
+
"selected_node": selected_node,
|
|
288
|
+
"selected_nodes": selected_nodes or None,
|
|
200
289
|
"confidence": self.confidence,
|
|
201
290
|
"stop_reason": self.stop_reason.value,
|
|
202
291
|
}
|
|
203
292
|
|
|
204
293
|
|
|
294
|
+
def _normalize_enum_value(value: Any) -> Any:
|
|
295
|
+
"""Normalize enum values into raw primitives.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
value : Any
|
|
300
|
+
Value to normalize.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
Any
|
|
305
|
+
Primitive value suitable for summaries.
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(value, Enum):
|
|
308
|
+
return value.value
|
|
309
|
+
return value
|
|
310
|
+
|
|
311
|
+
|
|
205
312
|
class ClassificationResult(StructureBase):
|
|
206
313
|
"""Represent the final result of taxonomy traversal.
|
|
207
314
|
|
|
208
315
|
Attributes
|
|
209
316
|
----------
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
317
|
+
final_node : TaxonomyNode or None
|
|
318
|
+
Resolved taxonomy node for the final selection.
|
|
319
|
+
final_nodes : list[TaxonomyNode] or None
|
|
320
|
+
Resolved taxonomy nodes for the final selections across branches.
|
|
214
321
|
confidence : float or None
|
|
215
322
|
Confidence score for the final selection.
|
|
216
323
|
stop_reason : ClassificationStopReason
|
|
217
324
|
Reason the traversal ended.
|
|
218
325
|
path : list[ClassificationStep]
|
|
219
326
|
Ordered list of classification steps.
|
|
327
|
+
path_nodes : list[TaxonomyNode]
|
|
328
|
+
Resolved taxonomy nodes selected across the path.
|
|
220
329
|
|
|
221
330
|
Methods
|
|
222
331
|
-------
|
|
223
332
|
depth
|
|
224
333
|
Return the number of classification steps recorded.
|
|
225
|
-
|
|
226
|
-
Return the
|
|
334
|
+
path_identifiers
|
|
335
|
+
Return the identifiers selected at each step.
|
|
336
|
+
|
|
337
|
+
Examples
|
|
338
|
+
--------
|
|
339
|
+
Summarize single and multi-class output:
|
|
340
|
+
|
|
341
|
+
>>> node = TaxonomyNode(label="Tax")
|
|
342
|
+
>>> result = ClassificationResult(
|
|
343
|
+
... final_node=node,
|
|
344
|
+
... final_nodes=[node],
|
|
345
|
+
... confidence=0.91,
|
|
346
|
+
... stop_reason=ClassificationStopReason.STOP,
|
|
347
|
+
... )
|
|
348
|
+
>>> result.final_nodes
|
|
349
|
+
[TaxonomyNode(label='Tax', description=None, children=[])]
|
|
227
350
|
"""
|
|
228
351
|
|
|
229
|
-
|
|
230
|
-
"
|
|
231
|
-
description="
|
|
352
|
+
final_node: TaxonomyNode | None = spec_field(
|
|
353
|
+
"final_node",
|
|
354
|
+
description="Resolved taxonomy node for the final selection.",
|
|
232
355
|
default=None,
|
|
233
356
|
)
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
description="
|
|
357
|
+
final_nodes: list[TaxonomyNode] | None = spec_field(
|
|
358
|
+
"final_nodes",
|
|
359
|
+
description="Resolved taxonomy nodes for the final selections.",
|
|
237
360
|
default=None,
|
|
238
361
|
)
|
|
239
362
|
confidence: Optional[float] = spec_field(
|
|
@@ -251,6 +374,11 @@ class ClassificationResult(StructureBase):
|
|
|
251
374
|
description="Ordered list of classification steps.",
|
|
252
375
|
default_factory=list,
|
|
253
376
|
)
|
|
377
|
+
path_nodes: list[TaxonomyNode] = spec_field(
|
|
378
|
+
"path_nodes",
|
|
379
|
+
description="Resolved taxonomy nodes selected across the path.",
|
|
380
|
+
default_factory=list,
|
|
381
|
+
)
|
|
254
382
|
|
|
255
383
|
@property
|
|
256
384
|
def depth(self) -> int:
|
|
@@ -264,15 +392,35 @@ class ClassificationResult(StructureBase):
|
|
|
264
392
|
return len(self.path)
|
|
265
393
|
|
|
266
394
|
@property
|
|
267
|
-
def
|
|
268
|
-
"""Return the
|
|
395
|
+
def path_identifiers(self) -> list[str]:
|
|
396
|
+
"""Return the identifiers selected at each step.
|
|
269
397
|
|
|
270
398
|
Returns
|
|
271
399
|
-------
|
|
272
400
|
list[str]
|
|
273
|
-
|
|
401
|
+
Identifiers selected at each classification step.
|
|
402
|
+
|
|
403
|
+
Examples
|
|
404
|
+
--------
|
|
405
|
+
>>> steps = [
|
|
406
|
+
... ClassificationStep(selected_node="Root"),
|
|
407
|
+
... ClassificationStep(selected_nodes=["Root > Leaf", "Root > Branch"]),
|
|
408
|
+
... ]
|
|
409
|
+
>>> ClassificationResult(
|
|
410
|
+
... stop_reason=ClassificationStopReason.STOP,
|
|
411
|
+
... path=steps,
|
|
412
|
+
... ).path_identifiers
|
|
413
|
+
['Root', 'Root > Leaf', 'Root > Branch']
|
|
274
414
|
"""
|
|
275
|
-
|
|
415
|
+
identifiers: list[str] = []
|
|
416
|
+
for step in self.path:
|
|
417
|
+
if step.selected_nodes:
|
|
418
|
+
identifiers.extend(
|
|
419
|
+
_normalize_enum_value(value) for value in step.selected_nodes
|
|
420
|
+
)
|
|
421
|
+
elif step.selected_node:
|
|
422
|
+
identifiers.append(_normalize_enum_value(step.selected_node))
|
|
423
|
+
return [identifier for identifier in identifiers if identifier]
|
|
276
424
|
|
|
277
425
|
|
|
278
426
|
def flatten_taxonomy(nodes: Iterable[TaxonomyNode]) -> list[TaxonomyNode]:
|
|
@@ -29,6 +29,8 @@ class AgentEnum(CrosswalkJSONEnum):
|
|
|
29
29
|
Translation agent for language conversion.
|
|
30
30
|
VALIDATOR : str
|
|
31
31
|
Validation agent for checking constraints and guardrails.
|
|
32
|
+
CLASSIFIER : str
|
|
33
|
+
Taxonomy classifier agent for structured label selection.
|
|
32
34
|
PLANNER : str
|
|
33
35
|
Meta-planning agent for generating execution plans.
|
|
34
36
|
DESIGNER : str
|
|
@@ -58,6 +60,7 @@ class AgentEnum(CrosswalkJSONEnum):
|
|
|
58
60
|
SUMMARIZER = "SummarizerAgent"
|
|
59
61
|
TRANSLATOR = "TranslatorAgent"
|
|
60
62
|
VALIDATOR = "ValidatorAgent"
|
|
63
|
+
CLASSIFIER = "TaxonomyClassifierAgent"
|
|
61
64
|
PLANNER = "MetaPlanner"
|
|
62
65
|
DESIGNER = "AgentDesigner"
|
|
63
66
|
BUILDER = "AgentBuilder"
|
|
@@ -89,6 +92,7 @@ class AgentEnum(CrosswalkJSONEnum):
|
|
|
89
92
|
"SUMMARIZER": {"value": "SummarizerAgent"},
|
|
90
93
|
"TRANSLATOR": {"value": "TranslatorAgent"},
|
|
91
94
|
"VALIDATOR": {"value": "ValidatorAgent"},
|
|
95
|
+
"CLASSIFIER": {"value": "TaxonomyClassifierAgent"},
|
|
92
96
|
"PLANNER": {"value": "MetaPlanner"},
|
|
93
97
|
"DESIGNER": {"value": "AgentDesigner"},
|
|
94
98
|
"BUILDER": {"value": "AgentBuilder"},
|