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.
@@ -1,9 +1,14 @@
1
1
  You are a taxonomy classification assistant.
2
2
 
3
3
  Instructions:
4
- - Review the text and select the best matching taxonomy node from the list.
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 selected_id empty.
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.selected_label }} (id={{ step.selected_id }}, confidence={{ step.confidence }})
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
- - id: {{ node.id }}
28
+ - identifier: {{ node.identifier }}
24
29
  label: {{ node.label }}
25
30
  description: {{ node.description or "None" }}
26
31
  {% endfor %}
@@ -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 != "$ref":
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
- def _resolve_ref(
486
- ref: str,
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 or None
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
- child_by_id(node_id)
34
- Return the child node matching the provided identifier.
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: Optional[str] = spec_field(
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 child_by_id(self, node_id: str | None) -> Optional["TaxonomyNode"]:
92
- """Return the child node matching the provided identifier.
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
- node_id : str or None
97
- Identifier of the child node to locate.
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 node_id is None:
103
+ if path is None:
105
104
  return None
106
- return next((child for child in self.children if child.id == node_id), None)
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 single classification step within a taxonomy level.
170
+ """Represent a classification step constrained to taxonomy node enums.
143
171
 
144
172
  Attributes
145
173
  ----------
146
- selected_id : str or None
147
- Identifier of the selected taxonomy node.
148
- selected_label : str or None
149
- Label of the selected taxonomy node.
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
- selected_id: Optional[str] = spec_field(
164
- "selected_id",
165
- description="Identifier of the selected taxonomy node.",
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
- selected_label: Optional[str] = spec_field(
169
- "selected_label",
170
- description="Label of the selected taxonomy node.",
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
- "selected_id": self.selected_id,
199
- "selected_label": self.selected_label,
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
- final_id : str or None
211
- Identifier of the final taxonomy node selection.
212
- final_label : str or None
213
- Label of the final taxonomy node selection.
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
- path_labels
226
- Return the labels selected at each step.
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
- final_id: Optional[str] = spec_field(
230
- "final_id",
231
- description="Identifier of the final taxonomy node selection.",
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
- final_label: Optional[str] = spec_field(
235
- "final_label",
236
- description="Label of the final taxonomy node selection.",
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 path_labels(self) -> list[str]:
268
- """Return the labels selected at each step.
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
- Labels selected at each classification step.
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
- return [step.selected_label for step in self.path if step.selected_label]
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"},