openai-sdk-helpers 0.6.4__py3-none-any.whl → 0.6.6__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.
@@ -26,6 +26,8 @@ from ..utils import ensure_list
26
26
  from .base import AgentBase
27
27
  from .configuration import AgentConfiguration
28
28
 
29
+ _CONTINUE_CONFIDENCE_THRESHOLD = 0.7
30
+
29
31
 
30
32
  class TaxonomyClassifierAgent(AgentBase):
31
33
  """Classify text by recursively traversing a taxonomy.
@@ -394,6 +396,13 @@ class TaxonomyClassifierAgent(AgentBase):
394
396
 
395
397
  resolved_nodes = _resolve_nodes(node_paths, step)
396
398
 
399
+ should_continue = _should_continue_from_stop(step, resolved_nodes)
400
+ if should_continue:
401
+ step = step.model_copy(
402
+ update={"stop_reason": ClassificationStopReason.CONTINUE}
403
+ )
404
+ state.steps[-1] = step
405
+
397
406
  if step.stop_reason.is_terminal:
398
407
  if resolved_nodes:
399
408
  state.final_nodes.extend(resolved_nodes)
@@ -407,7 +416,7 @@ class TaxonomyClassifierAgent(AgentBase):
407
416
  return
408
417
 
409
418
  base_steps_len = len(state.steps)
410
- child_tasks: list[tuple[Awaitable["_TraversalState"], int]] = []
419
+ child_tasks: list[tuple[Awaitable["_TraversalState"], int, TaxonomyNode]] = []
411
420
  for node in resolved_nodes:
412
421
  if node.children:
413
422
  sub_agent = self._build_sub_agent(list(node.children))
@@ -429,6 +438,7 @@ class TaxonomyClassifierAgent(AgentBase):
429
438
  state=sub_state,
430
439
  ),
431
440
  base_final_nodes_len,
441
+ node,
432
442
  )
433
443
  )
434
444
  else:
@@ -439,13 +449,19 @@ class TaxonomyClassifierAgent(AgentBase):
439
449
  )
440
450
  if child_tasks:
441
451
  child_states = await asyncio.gather(
442
- *(child_task for child_task, _ in child_tasks)
452
+ *(child_task for child_task, _, _ in child_tasks)
443
453
  )
444
- for child_state, (_, base_final_nodes_len) in zip(
454
+ for child_state, (_, base_final_nodes_len, parent_node) in zip(
445
455
  child_states, child_tasks, strict=True
446
456
  ):
447
457
  state.steps.extend(child_state.steps[base_steps_len:])
448
- state.final_nodes.extend(child_state.final_nodes[base_final_nodes_len:])
458
+ child_final_nodes = child_state.final_nodes[base_final_nodes_len:]
459
+ state.final_nodes.extend(child_final_nodes)
460
+ if should_continue and not child_final_nodes:
461
+ state.final_nodes.append(parent_node)
462
+ state.best_confidence = _max_confidence(
463
+ state.best_confidence, step.confidence
464
+ )
449
465
  state.best_confidence = _max_confidence(
450
466
  state.best_confidence, child_state.best_confidence
451
467
  )
@@ -627,6 +643,34 @@ def _resolve_stop_reason(state: _TraversalState) -> ClassificationStopReason:
627
643
  return ClassificationStopReason.NO_MATCH
628
644
 
629
645
 
646
+ def _should_continue_from_stop(
647
+ step: ClassificationStep,
648
+ resolved_nodes: Sequence[TaxonomyNode],
649
+ threshold: float = _CONTINUE_CONFIDENCE_THRESHOLD,
650
+ ) -> bool:
651
+ """Return True when a stop reason should continue traversal.
652
+
653
+ Parameters
654
+ ----------
655
+ step : ClassificationStep
656
+ Classification step to evaluate.
657
+ resolved_nodes : Sequence[TaxonomyNode]
658
+ Resolved taxonomy nodes from the classification step.
659
+ threshold : float, default=0.7
660
+ Confidence threshold for overriding a stop reason.
661
+
662
+ Returns
663
+ -------
664
+ bool
665
+ True when traversal should proceed despite a stop reason.
666
+ """
667
+ if step.stop_reason is not ClassificationStopReason.STOP:
668
+ return False
669
+ if step.confidence is None or step.confidence < threshold:
670
+ return False
671
+ return any(node.children for node in resolved_nodes)
672
+
673
+
630
674
  def _normalize_roots(
631
675
  taxonomy: TaxonomyNode | Sequence[TaxonomyNode],
632
676
  ) -> list[TaxonomyNode]:
@@ -174,6 +174,100 @@ def _ensure_schema_has_type(schema: dict[str, Any]) -> None:
174
174
  schema.update(_build_any_value_schema())
175
175
 
176
176
 
177
+ def _strip_ref_types(
178
+ schema: dict[str, Any],
179
+ *,
180
+ nullable_fields: set[str] | None = None,
181
+ ) -> None:
182
+ """Remove type entries from enum $ref nodes when non-nullable.
183
+
184
+ Parameters
185
+ ----------
186
+ schema : dict[str, Any]
187
+ Root schema to clean in place.
188
+ nullable_fields : set[str] | None, optional
189
+ Field names that should remain nullable. Defaults to None.
190
+ """
191
+ field_names = nullable_fields or set()
192
+
193
+ def _resolve_ref(ref: str) -> dict[str, Any] | None:
194
+ if not ref.startswith("#/"):
195
+ return None
196
+ pointer = ref.removeprefix("#/")
197
+ current: Any = schema
198
+ for part in pointer.split("/"):
199
+ if not isinstance(current, dict):
200
+ return None
201
+ current = current.get(part)
202
+ if isinstance(current, dict):
203
+ return current
204
+ return None
205
+
206
+ def _is_enum_ref(ref: str) -> bool:
207
+ ref_target = _resolve_ref(ref)
208
+ if not isinstance(ref_target, dict):
209
+ return False
210
+ return "enum" in ref_target
211
+
212
+ def _is_null_schema(entry: Any) -> bool:
213
+ if not isinstance(entry, dict):
214
+ return False
215
+ entry_type = entry.get("type")
216
+ if entry_type == "null":
217
+ return True
218
+ if isinstance(entry_type, list) and "null" in entry_type:
219
+ return True
220
+ return False
221
+
222
+ def _walk(
223
+ node: Any,
224
+ *,
225
+ nullable_context: bool = False,
226
+ nullable_property: bool = False,
227
+ ) -> None:
228
+ if isinstance(node, dict):
229
+ ref = node.get("$ref")
230
+ if (
231
+ isinstance(ref, str)
232
+ and _is_enum_ref(ref)
233
+ and not nullable_context
234
+ and not nullable_property
235
+ ):
236
+ node.pop("type", None)
237
+ for key, value in node.items():
238
+ if key == "anyOf" and isinstance(value, list):
239
+ anyof_nullable = any(_is_null_schema(entry) for entry in value)
240
+ for entry in value:
241
+ _walk(
242
+ entry,
243
+ nullable_context=anyof_nullable or nullable_context,
244
+ nullable_property=nullable_property,
245
+ )
246
+ continue
247
+ if key == "properties" and isinstance(value, dict):
248
+ for prop_name, prop_schema in value.items():
249
+ _walk(
250
+ prop_schema,
251
+ nullable_context=nullable_context,
252
+ nullable_property=prop_name in field_names,
253
+ )
254
+ continue
255
+ _walk(
256
+ value,
257
+ nullable_context=nullable_context,
258
+ nullable_property=nullable_property,
259
+ )
260
+ elif isinstance(node, list):
261
+ for item in node:
262
+ _walk(
263
+ item,
264
+ nullable_context=nullable_context,
265
+ nullable_property=nullable_property,
266
+ )
267
+
268
+ _walk(schema)
269
+
270
+
177
271
  def _hydrate_ref_types(schema: dict[str, Any]) -> None:
178
272
  """Attach explicit types to $ref nodes when available.
179
273
 
@@ -556,17 +650,18 @@ class StructureBase(BaseModelJSONSerializable):
556
650
 
557
651
  cleaned_schema = cast(dict[str, Any], clean_refs(schema))
558
652
 
559
- cleaned_schema = cast(dict[str, Any], cleaned_schema)
560
- _hydrate_ref_types(cleaned_schema)
561
- _ensure_items_have_schema(cleaned_schema)
562
- _ensure_schema_has_type(cleaned_schema)
563
-
564
653
  nullable_fields = {
565
654
  name
566
655
  for name, model_field in getattr(cls, "model_fields", {}).items()
567
656
  if getattr(model_field, "default", inspect.Signature.empty) is None
568
657
  }
569
658
 
659
+ cleaned_schema = cast(dict[str, Any], cleaned_schema)
660
+ _hydrate_ref_types(cleaned_schema)
661
+ _ensure_items_have_schema(cleaned_schema)
662
+ _ensure_schema_has_type(cleaned_schema)
663
+ _strip_ref_types(cleaned_schema, nullable_fields=nullable_fields)
664
+
570
665
  properties = cleaned_schema.get("properties", {})
571
666
  if isinstance(properties, dict) and nullable_fields:
572
667
  for field_name in nullable_fields:
@@ -480,6 +480,8 @@ class ClassificationResult(StructureBase):
480
480
  Yield selected identifiers across all steps.
481
481
  selected_nodes
482
482
  Return the selected identifiers across all steps.
483
+ to_lightweight_summary
484
+ Return a lightweight summary of selected node paths.
483
485
 
484
486
  Examples
485
487
  --------
@@ -565,6 +567,52 @@ class ClassificationResult(StructureBase):
565
567
  if normalized:
566
568
  yield normalized
567
569
 
570
+ def to_lightweight_summary(self) -> "ClassificationSummary | None":
571
+ """Return a lightweight summary of selected node paths.
572
+
573
+ Returns
574
+ -------
575
+ ClassificationSummary or None
576
+ Summary containing the ``full_paths`` of selected nodes, or None
577
+ when no selections exist.
578
+
579
+ Examples
580
+ --------
581
+ >>> result = ClassificationResult(steps=[])
582
+ >>> result.to_lightweight_summary() is None
583
+ True
584
+ """
585
+ full_paths = [
586
+ format_path_identifier(taxonomy_enum_path(node))
587
+ for node in self.selected_nodes
588
+ ]
589
+ if not full_paths:
590
+ return None
591
+ return ClassificationSummary(full_paths=full_paths)
592
+
593
+
594
+ class ClassificationSummary(StructureBase):
595
+ """Represent a lightweight summary of selected taxonomy paths.
596
+
597
+ Attributes
598
+ ----------
599
+ full_paths : list[str] or None
600
+ Selected taxonomy paths including parent segments.
601
+
602
+ Methods
603
+ -------
604
+ to_json()
605
+ Serialize the summary to a JSON-compatible dictionary.
606
+ from_json(data)
607
+ Construct a summary instance from JSON-compatible data.
608
+ """
609
+
610
+ full_paths: list[str] | None = spec_field(
611
+ "full_paths",
612
+ description="Selected taxonomy paths including parent segments.",
613
+ default=None,
614
+ )
615
+
568
616
 
569
617
  def taxonomy_enum_path(value: Enum | str | None) -> list[str]:
570
618
  """Return the taxonomy path segments for an enum value.
@@ -598,6 +646,7 @@ def taxonomy_enum_path(value: Enum | str | None) -> list[str]:
598
646
 
599
647
  __all__ = [
600
648
  "ClassificationResult",
649
+ "ClassificationSummary",
601
650
  "ClassificationStep",
602
651
  "ClassificationStopReason",
603
652
  "Taxonomy",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openai-sdk-helpers
3
- Version: 0.6.4
3
+ Version: 0.6.6
4
4
  Summary: Composable helpers for OpenAI SDK agents, prompts, and storage
5
5
  Author: openai-sdk-helpers maintainers
6
6
  License: MIT
@@ -10,7 +10,7 @@ openai_sdk_helpers/tools.py,sha256=8hhcytpmDfoXV16UQbDmDVV0rhLOn8c_VjXO8XaTFLQ,1
10
10
  openai_sdk_helpers/types.py,sha256=ejCG0rYqJhjOQvKLoNnzq-TzcKCFt69GVfi7y805NkU,1451
11
11
  openai_sdk_helpers/agent/__init__.py,sha256=qyzKzPhD8KsEl6d79XERK32AK5It_BZNOqChOpBdmhg,1199
12
12
  openai_sdk_helpers/agent/base.py,sha256=vLs0oALhxsd_Xy5dGjSZTUFTug-YwZkF1LabQ2ruLxk,29508
13
- openai_sdk_helpers/agent/classifier.py,sha256=PHUnA5dSDWBQeRxwo0Qe8xIe7Ren3xSRAOmsQrRK_oA,33241
13
+ openai_sdk_helpers/agent/classifier.py,sha256=PyeLNnu2cHx_zjY8ReLV2AN5olc0LZuvh6urNZgK6jc,34795
14
14
  openai_sdk_helpers/agent/configuration.py,sha256=ZeH4ErgVe-BZamjUeNONbQi60ViolgYAWh-c8hNAQTw,15810
15
15
  openai_sdk_helpers/agent/coordinator.py,sha256=lVjA0yI-GhGKlqbNR_k9GOCrUjFoZ0QoqRaafHckyME,18052
16
16
  openai_sdk_helpers/agent/files.py,sha256=H7UfSZSjFUbv1cjRvNld9kZwIjc5wPq4vynqU8HgGJE,4478
@@ -57,8 +57,8 @@ openai_sdk_helpers/streamlit_app/app.py,sha256=kkjtdCKVwrJ9nZWuBArm3dhvcjMESX0TM
57
57
  openai_sdk_helpers/streamlit_app/configuration.py,sha256=0KeJ4HqCNFthBHsedV6ptqHluAcTPBb5_TujFOGkIUU,16685
58
58
  openai_sdk_helpers/structure/__init__.py,sha256=w27ezTYVLzZdDMFfA8mawE82h8zO53idFBCiCfYfh7s,4321
59
59
  openai_sdk_helpers/structure/agent_blueprint.py,sha256=VyJWkgPNzAYKRDMeR1M4kE6qqQURnwqtrrEn0TRJf0g,9698
60
- openai_sdk_helpers/structure/base.py,sha256=UrnNNU9qQ9mEES8MB9y6QESbDgPXH47XW8LVWSxYUYM,25280
61
- openai_sdk_helpers/structure/classification.py,sha256=SYrrsv0Y2A2kXhL3jbn7lWnTb5jB_UE-cx-sJSRCxEA,17312
60
+ openai_sdk_helpers/structure/base.py,sha256=OVI305F2suG6c2Qh_ZD_wZ1mS1GpqPBC-4RqInXqiAU,28512
61
+ openai_sdk_helpers/structure/classification.py,sha256=aIelEz3Ffj4CKd0P_EB4uOMr0yX4uqqdKcRP9hjY0nw,18769
62
62
  openai_sdk_helpers/structure/extraction.py,sha256=wODP0iLAhhsdQkMWRYPYTiLUMU8bFMKiBjPl3PKUleg,37335
63
63
  openai_sdk_helpers/structure/prompt.py,sha256=ZfsaHdA0hj5zmZDrOdpXjCsC8U-jjzwFG4JBsWYiaH4,1535
64
64
  openai_sdk_helpers/structure/responses.py,sha256=WUwh0DhXj24pkvgqH1FMkdx5V2ArdvdtrDN_fuMBtDU,4882
@@ -92,8 +92,8 @@ openai_sdk_helpers/vector_storage/__init__.py,sha256=L5LxO09puh9_yBB9IDTvc1CvVkA
92
92
  openai_sdk_helpers/vector_storage/cleanup.py,sha256=sZ4ZSTlnjF52o9Cc8A9dTX37ZYXXDxS_fdIpoOBWvrg,3666
93
93
  openai_sdk_helpers/vector_storage/storage.py,sha256=t_ukacaXRa9EXE4-3BxsrB4Rjhu6nTu7NA9IjCJBIpQ,24259
94
94
  openai_sdk_helpers/vector_storage/types.py,sha256=jTCcOYMeOpZWvcse0z4T3MVs-RBOPC-fqWTBeQrgafU,1639
95
- openai_sdk_helpers-0.6.4.dist-info/METADATA,sha256=l5XBsVFPOrOSDskGR0ZhgKHjJFLtO_-ZgWrRjXi1_bU,24622
96
- openai_sdk_helpers-0.6.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
97
- openai_sdk_helpers-0.6.4.dist-info/entry_points.txt,sha256=gEOD1ZeXe8d2OP-KzUlG-b_9D9yUZTCt-GFW3EDbIIY,63
98
- openai_sdk_helpers-0.6.4.dist-info/licenses/LICENSE,sha256=CUhc1NrE50bs45tcXF7OcTQBKEvkUuLqeOHgrWQ5jaA,1067
99
- openai_sdk_helpers-0.6.4.dist-info/RECORD,,
95
+ openai_sdk_helpers-0.6.6.dist-info/METADATA,sha256=cyg4hIimWHNrkSxsEwWZGOOmaBFAWtSHfL9JkGQXHxw,24622
96
+ openai_sdk_helpers-0.6.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
97
+ openai_sdk_helpers-0.6.6.dist-info/entry_points.txt,sha256=gEOD1ZeXe8d2OP-KzUlG-b_9D9yUZTCt-GFW3EDbIIY,63
98
+ openai_sdk_helpers-0.6.6.dist-info/licenses/LICENSE,sha256=CUhc1NrE50bs45tcXF7OcTQBKEvkUuLqeOHgrWQ5jaA,1067
99
+ openai_sdk_helpers-0.6.6.dist-info/RECORD,,