openai-sdk-helpers 0.4.3__py3-none-any.whl → 0.5.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.
Files changed (47) hide show
  1. openai_sdk_helpers/__init__.py +41 -7
  2. openai_sdk_helpers/agent/__init__.py +1 -2
  3. openai_sdk_helpers/agent/base.py +89 -173
  4. openai_sdk_helpers/agent/configuration.py +12 -20
  5. openai_sdk_helpers/agent/coordinator.py +14 -17
  6. openai_sdk_helpers/agent/runner.py +3 -45
  7. openai_sdk_helpers/agent/search/base.py +49 -71
  8. openai_sdk_helpers/agent/search/vector.py +82 -110
  9. openai_sdk_helpers/agent/search/web.py +103 -81
  10. openai_sdk_helpers/agent/summarizer.py +20 -28
  11. openai_sdk_helpers/agent/translator.py +17 -23
  12. openai_sdk_helpers/agent/validator.py +17 -23
  13. openai_sdk_helpers/errors.py +9 -0
  14. openai_sdk_helpers/extract/__init__.py +23 -0
  15. openai_sdk_helpers/extract/extractor.py +157 -0
  16. openai_sdk_helpers/extract/generator.py +476 -0
  17. openai_sdk_helpers/prompt/extractor_config_agent_instructions.jinja +6 -0
  18. openai_sdk_helpers/prompt/extractor_config_generator.jinja +37 -0
  19. openai_sdk_helpers/prompt/extractor_config_generator_instructions.jinja +9 -0
  20. openai_sdk_helpers/prompt/extractor_prompt_optimizer_agent_instructions.jinja +4 -0
  21. openai_sdk_helpers/prompt/extractor_prompt_optimizer_request.jinja +11 -0
  22. openai_sdk_helpers/response/__init__.py +2 -6
  23. openai_sdk_helpers/response/base.py +85 -94
  24. openai_sdk_helpers/response/configuration.py +39 -14
  25. openai_sdk_helpers/response/files.py +2 -0
  26. openai_sdk_helpers/response/runner.py +1 -48
  27. openai_sdk_helpers/response/tool_call.py +0 -141
  28. openai_sdk_helpers/response/vector_store.py +8 -5
  29. openai_sdk_helpers/streamlit_app/app.py +1 -1
  30. openai_sdk_helpers/structure/__init__.py +16 -0
  31. openai_sdk_helpers/structure/base.py +239 -278
  32. openai_sdk_helpers/structure/extraction.py +1228 -0
  33. openai_sdk_helpers/structure/plan/plan.py +0 -20
  34. openai_sdk_helpers/structure/plan/task.py +0 -33
  35. openai_sdk_helpers/structure/prompt.py +16 -0
  36. openai_sdk_helpers/structure/responses.py +2 -2
  37. openai_sdk_helpers/structure/web_search.py +0 -10
  38. openai_sdk_helpers/tools.py +346 -99
  39. openai_sdk_helpers/utils/__init__.py +7 -0
  40. openai_sdk_helpers/utils/json/base_model.py +315 -32
  41. openai_sdk_helpers/utils/langextract.py +194 -0
  42. {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.0.dist-info}/METADATA +18 -4
  43. {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.0.dist-info}/RECORD +46 -37
  44. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +0 -75
  45. {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.0.dist-info}/WHEEL +0 -0
  46. {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.0.dist-info}/entry_points.txt +0 -0
  47. {openai_sdk_helpers-0.4.3.dist-info → openai_sdk_helpers-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,33 +8,158 @@ generation, validation, and serialization.
8
8
  from __future__ import annotations
9
9
 
10
10
  # Standard library imports
11
- import ast
11
+ import copy
12
+ import dataclasses
12
13
  import inspect
13
14
  import json
14
- import logging
15
15
  from dataclasses import dataclass
16
- from enum import Enum
17
16
  from pathlib import Path
18
17
  from typing import (
19
18
  Any,
20
19
  ClassVar,
20
+ Mapping,
21
21
  TypeVar,
22
22
  cast,
23
- get_args,
24
- get_origin,
25
23
  )
26
24
 
27
25
  # Third-party imports
28
26
  from pydantic import BaseModel, ConfigDict, Field
29
27
  from openai.types.responses.response_text_config_param import ResponseTextConfigParam
30
28
 
31
-
32
29
  # Internal imports
33
30
 
34
- from ..utils import check_filepath, log, BaseModelJSONSerializable
31
+ from ..utils import check_filepath, BaseModelJSONSerializable
35
32
 
36
33
  T = TypeVar("T", bound="StructureBase")
37
- DEFAULT_DATA_PATH: Path | None = None
34
+
35
+
36
+ def _add_required_fields(target: dict[str, Any]) -> None:
37
+ """Ensure every object declares its required properties."""
38
+ properties = target.get("properties")
39
+ if isinstance(properties, dict) and properties:
40
+ target["required"] = sorted(properties.keys())
41
+ for value in target.values():
42
+ if isinstance(value, dict):
43
+ _add_required_fields(value)
44
+ elif isinstance(value, list):
45
+ for item in value:
46
+ if isinstance(item, dict):
47
+ _add_required_fields(item)
48
+
49
+
50
+ def _enforce_additional_properties(target: Any) -> None:
51
+ """Ensure every object schema disallows additional properties."""
52
+ if isinstance(target, dict):
53
+ schema_type = target.get("type")
54
+ allows_object_type = schema_type == "object" or (
55
+ isinstance(schema_type, list)
56
+ and "object" in schema_type
57
+ and set(schema_type).issubset({"object", "null"})
58
+ )
59
+ if (allows_object_type or "properties" in target) and "$ref" not in target:
60
+ target.setdefault("properties", {})
61
+ target["additionalProperties"] = False
62
+ any_of = target.get("anyOf")
63
+ if isinstance(any_of, list):
64
+ for entry in any_of:
65
+ if not isinstance(entry, dict):
66
+ continue
67
+ entry_type = entry.get("type")
68
+ entry_allows_object_type = entry_type == "object" or (
69
+ isinstance(entry_type, list)
70
+ and "object" in entry_type
71
+ and set(entry_type).issubset({"object", "null"})
72
+ )
73
+ if (
74
+ entry_allows_object_type or "properties" in entry
75
+ ) and "$ref" not in entry:
76
+ entry.setdefault("properties", {})
77
+ entry["additionalProperties"] = False
78
+ for value in target.values():
79
+ _enforce_additional_properties(value)
80
+ elif isinstance(target, list):
81
+ for item in target:
82
+ _enforce_additional_properties(item)
83
+
84
+
85
+ def _build_any_value_schema(depth: int = 0) -> dict[str, Any]:
86
+ """Return a JSON schema fragment describing a permissive JSON value.
87
+
88
+ Parameters
89
+ ----------
90
+ depth : int, optional
91
+ Current recursion depth for nested arrays. Defaults to 0.
92
+
93
+ Returns
94
+ -------
95
+ dict[str, Any]
96
+ JSON schema fragment describing a permissive value.
97
+ """
98
+ value_types = ["string", "integer", "number", "null"]
99
+ any_of: list[dict[str, Any]] = [{"type": value_type} for value_type in value_types]
100
+
101
+ any_of.append(
102
+ {
103
+ "type": "object",
104
+ "properties": {},
105
+ "additionalProperties": False,
106
+ }
107
+ )
108
+ if depth < 1:
109
+ any_of.append(
110
+ {
111
+ "type": "array",
112
+ "items": _build_any_value_schema(depth + 1),
113
+ }
114
+ )
115
+
116
+ return {"anyOf": any_of}
117
+
118
+
119
+ def _ensure_items_have_schema(target: Any) -> None:
120
+ """Ensure array item schemas include type information."""
121
+ if isinstance(target, dict):
122
+ items = target.get("items")
123
+ if isinstance(items, dict):
124
+ if not items:
125
+ target["items"] = _build_any_value_schema()
126
+ else:
127
+ _ensure_schema_has_type(items)
128
+ for value in target.values():
129
+ _ensure_items_have_schema(value)
130
+ elif isinstance(target, list):
131
+ for item in target:
132
+ _ensure_items_have_schema(item)
133
+
134
+
135
+ def _ensure_schema_has_type(schema: dict[str, Any]) -> None:
136
+ """Ensure a schema dictionary includes a type entry when possible."""
137
+ if "type" in schema or "$ref" in schema:
138
+ return
139
+ any_of = schema.get("anyOf")
140
+ if isinstance(any_of, list):
141
+ inferred_types: set[str] = set()
142
+ for entry in any_of:
143
+ if not isinstance(entry, dict):
144
+ continue
145
+ entry_type = entry.get("type")
146
+ if isinstance(entry_type, str):
147
+ inferred_types.add(entry_type)
148
+ elif isinstance(entry_type, list):
149
+ inferred_types.update(
150
+ element for element in entry_type if isinstance(element, str)
151
+ )
152
+ if inferred_types:
153
+ schema["type"] = sorted(inferred_types)
154
+ return
155
+ if "properties" in schema:
156
+ schema["type"] = "object"
157
+ schema.setdefault("additionalProperties", False)
158
+ return
159
+ if "items" in schema:
160
+ schema["type"] = "array"
161
+ return
162
+ schema.update(_build_any_value_schema())
38
163
 
39
164
 
40
165
  class StructureBase(BaseModelJSONSerializable):
@@ -51,9 +176,9 @@ class StructureBase(BaseModelJSONSerializable):
51
176
 
52
177
  Attributes
53
178
  ----------
54
- DATA_PATH : Path or None, class attribute
55
- Optional location for saving schema files. Set at class level
56
- before calling save_schema_to_file.
179
+ model_config : ConfigDict
180
+ Pydantic model configuration with strict validation and enum handling.
181
+
57
182
 
58
183
  Methods
59
184
  -------
@@ -85,8 +210,6 @@ class StructureBase(BaseModelJSONSerializable):
85
210
  Produce Field overrides for dynamic schema customization.
86
211
  print()
87
212
  Return a string representation of the structure.
88
- console_print()
89
- Print the string representation to stdout.
90
213
 
91
214
  Examples
92
215
  --------
@@ -113,10 +236,9 @@ class StructureBase(BaseModelJSONSerializable):
113
236
  """
114
237
 
115
238
  model_config = ConfigDict(
116
- title="OutputStructure", use_enum_values=False, strict=True, extra="forbid"
239
+ title=__qualname__, use_enum_values=False, strict=True, extra="forbid"
117
240
  )
118
- DATA_PATH: ClassVar[Path | None] = DEFAULT_DATA_PATH
119
- """Optional location for saving schema files."""
241
+ _schema_cache: ClassVar[dict[type["StructureBase"], dict[str, Any]]] = {}
120
242
 
121
243
  @classmethod
122
244
  def get_prompt(cls, add_enum_values: bool = True) -> str:
@@ -137,31 +259,6 @@ class StructureBase(BaseModelJSONSerializable):
137
259
  return "No structured prompt available."
138
260
  return "# Output Format\n" + "\n".join(prompt_lines)
139
261
 
140
- @classmethod
141
- def _get_all_fields(cls) -> dict[Any, Any]:
142
- """Collect all fields from the class hierarchy including inherited ones.
143
-
144
- Traverses the method resolution order (MRO) to gather fields from
145
- all parent classes that inherit from BaseModel, ensuring inherited
146
- fields are included in schema generation.
147
-
148
- Results are computed once per class and cached for performance.
149
-
150
- Returns
151
- -------
152
- dict[Any, Any]
153
- Mapping of field names to Pydantic ModelField instances.
154
- """
155
- # Use class-level caching for performance
156
- cache_attr = "_all_fields_cache"
157
- if not hasattr(cls, cache_attr):
158
- fields = {}
159
- for base in reversed(cls.__mro__): # Traverse inheritance tree
160
- if issubclass(base, BaseModel) and hasattr(base, "model_fields"):
161
- fields.update(base.model_fields) # Merge fields from parent
162
- setattr(cls, cache_attr, fields)
163
- return getattr(cls, cache_attr)
164
-
165
262
  @classmethod
166
263
  def _get_field_prompt(
167
264
  cls, field_name: str, field, add_enum_values: bool = True
@@ -210,12 +307,12 @@ class StructureBase(BaseModelJSONSerializable):
210
307
  )
211
308
 
212
309
  @classmethod
213
- def get_input_prompt_list(cls, add_enum_values: bool = True) -> list[str]:
310
+ def get_input_prompt_list(cls, add_enums: bool = True) -> list[str]:
214
311
  """Dynamically build a structured prompt including inherited fields.
215
312
 
216
313
  Parameters
217
314
  ----------
218
- add_enum_values : bool, default=True
315
+ add_enums : bool, default=True
219
316
  Whether enumeration values should be included.
220
317
 
221
318
  Returns
@@ -226,9 +323,7 @@ class StructureBase(BaseModelJSONSerializable):
226
323
  prompt_lines = []
227
324
  all_fields = cls._get_all_fields()
228
325
  for field_name, field in all_fields.items():
229
- prompt_lines.append(
230
- cls._get_field_prompt(field_name, field, add_enum_values)
231
- )
326
+ prompt_lines.append(cls._get_field_prompt(field_name, field, add_enums))
232
327
  return prompt_lines
233
328
 
234
329
  @classmethod
@@ -282,7 +377,9 @@ class StructureBase(BaseModelJSONSerializable):
282
377
  return assistant_format(cls)
283
378
 
284
379
  @classmethod
285
- def response_tool_definition(cls, tool_name: str, *, tool_description: str) -> dict:
380
+ def response_tool_definition(
381
+ cls, tool_name: str, *, tool_description: str | None
382
+ ) -> dict:
286
383
  """Build a chat completion tool definition for this structure.
287
384
 
288
385
  Creates a function tool definition compatible with the chat
@@ -357,12 +454,17 @@ class StructureBase(BaseModelJSONSerializable):
357
454
  - Adds null type for fields with None default
358
455
  - Cleans up $ref entries for better compatibility
359
456
  - Recursively processes nested structures
457
+ - Caches the computed schema per class
360
458
 
361
459
  Examples
362
460
  --------
363
461
  >>> schema = MyStructure.get_schema()
364
462
  >>> print(json.dumps(schema, indent=2))
365
463
  """
464
+ cached_schema = cls._schema_cache.get(cls)
465
+ if cached_schema is not None:
466
+ return copy.deepcopy(cached_schema)
467
+
366
468
  schema = cls.model_json_schema()
367
469
 
368
470
  def clean_refs(obj: Any) -> Any:
@@ -380,18 +482,60 @@ class StructureBase(BaseModelJSONSerializable):
380
482
 
381
483
  cleaned_schema = cast(dict[str, Any], clean_refs(schema))
382
484
 
383
- def add_required_fields(target: dict[str, Any]) -> None:
384
- """Ensure every object declares its required properties."""
385
- properties = target.get("properties")
386
- if isinstance(properties, dict) and properties:
387
- target["required"] = sorted(properties.keys())
388
- for value in target.values():
389
- if isinstance(value, dict):
390
- add_required_fields(value)
391
- elif isinstance(value, list):
392
- for item in value:
393
- if isinstance(item, dict):
394
- add_required_fields(item)
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
+ )
538
+ _ensure_items_have_schema(cleaned_schema)
395
539
 
396
540
  nullable_fields = {
397
541
  name
@@ -422,242 +566,41 @@ class StructureBase(BaseModelJSONSerializable):
422
566
  if not has_null:
423
567
  any_of.append({"type": "null"})
424
568
 
425
- add_required_fields(cleaned_schema)
426
- return cleaned_schema
569
+ _add_required_fields(cleaned_schema)
570
+ _enforce_additional_properties(cleaned_schema)
571
+ cls._schema_cache[cls] = cleaned_schema
572
+ return copy.deepcopy(cleaned_schema)
427
573
 
428
574
  @classmethod
429
- def save_schema_to_file(cls) -> Path:
575
+ def save_schema_to_file(cls, file_path: Path) -> Path:
430
576
  """Save the generated JSON schema to a file.
431
577
 
432
- Generates the schema using get_schema and saves it to a JSON file
433
- within the DATA_PATH directory. The filename is derived from the
434
- class name.
578
+ Generates the schema using get_schema and saves it to the provided
579
+ file path.
580
+
581
+ Parameters
582
+ ----------
583
+ file_path : Path
584
+ Full path (including filename) where the schema should be saved.
435
585
 
436
586
  Returns
437
587
  -------
438
588
  Path
439
589
  Absolute path to the saved schema file.
440
590
 
441
- Raises
442
- ------
443
- RuntimeError
444
- If DATA_PATH is not set on the class.
445
591
 
446
592
  Examples
447
593
  --------
448
594
  >>> MyStructure.DATA_PATH = Path("./schemas")
449
- >>> schema_path = MyStructure.save_schema_to_file()
595
+ >>> schema_path = MyStructure.save_schema_to_file(file_path=MyStructure.DATA_PATH / "MyStructure_schema.json")
450
596
  >>> print(schema_path)
451
597
  PosixPath('./schemas/MyStructure_schema.json')
452
598
  """
453
- schema = cls.get_schema()
454
- if cls.DATA_PATH is None:
455
- raise RuntimeError(
456
- "DATA_PATH is not set. Set StructureBase.DATA_PATH before saving."
457
- )
458
- file_path = cls.DATA_PATH / f"{cls.__name__}_schema.json"
459
599
  check_filepath(file_path)
460
600
  with file_path.open("w", encoding="utf-8") as file_handle:
461
- json.dump(schema, file_handle, indent=2, ensure_ascii=False)
601
+ json.dump(cls.get_schema(), file_handle, indent=2, ensure_ascii=False)
462
602
  return file_path
463
603
 
464
- @classmethod
465
- def _extract_enum_class(cls, field_type: Any) -> type[Enum] | None:
466
- """Extract an Enum class from a field's type annotation.
467
-
468
- Handles direct Enum types, list[Enum], and optional Enums.
469
-
470
- Parameters
471
- ----------
472
- field_type : Any
473
- Type annotation of a field.
474
-
475
- Returns
476
- -------
477
- type[Enum] or None
478
- Enum class if found, otherwise None.
479
- """
480
- origin = get_origin(field_type)
481
- args = get_args(field_type)
482
-
483
- if inspect.isclass(field_type) and issubclass(field_type, Enum):
484
- return field_type
485
- elif (
486
- origin is list
487
- and args
488
- and inspect.isclass(args[0])
489
- and issubclass(args[0], Enum)
490
- ):
491
- return args[0]
492
- elif origin is not None:
493
- # Handle Union types
494
- for arg in args:
495
- enum_cls = cls._extract_enum_class(arg)
496
- if enum_cls:
497
- return enum_cls
498
- return None
499
-
500
- @classmethod
501
- def _build_enum_field_mapping(cls) -> dict[str, type[Enum]]:
502
- """Build a mapping from field names to their Enum classes.
503
-
504
- Used by from_raw_input to correctly process enum values from
505
- raw API responses.
506
-
507
- Returns
508
- -------
509
- dict[str, type[Enum]]
510
- Mapping of field names to Enum types.
511
- """
512
- mapping: dict[str, type[Enum]] = {}
513
-
514
- for name, model_field in cls.model_fields.items():
515
- field_type = model_field.annotation
516
- enum_cls = cls._extract_enum_class(field_type)
517
-
518
- if enum_cls is not None:
519
- mapping[name] = enum_cls
520
-
521
- return mapping
522
-
523
- @classmethod
524
- def from_raw_input(cls: type[T], data: dict) -> T:
525
- """Construct an instance from a dictionary of raw input data.
526
-
527
- Particularly useful for converting data from OpenAI API tool calls
528
- or assistant outputs into validated structure instances. Handles
529
- enum value conversion automatically.
530
-
531
- Parameters
532
- ----------
533
- data : dict
534
- Raw input data dictionary from API response.
535
-
536
- Returns
537
- -------
538
- T
539
- Validated instance of the structure class.
540
-
541
- Examples
542
- --------
543
- >>> raw_data = {"title": "Test", "score": 0.95}
544
- >>> instance = MyStructure.from_raw_input(raw_data)
545
- """
546
- mapping = cls._build_enum_field_mapping()
547
- clean_data = data.copy()
548
-
549
- for field, enum_cls in mapping.items():
550
- raw_value = clean_data.get(field)
551
-
552
- if raw_value is None:
553
- continue
554
-
555
- # List of enum values
556
- if isinstance(raw_value, list):
557
- converted = []
558
- for v in raw_value:
559
- if isinstance(v, enum_cls):
560
- converted.append(v)
561
- elif isinstance(v, str):
562
- # Check if it's a valid value
563
- if v in enum_cls._value2member_map_:
564
- converted.append(enum_cls(v))
565
- # Check if it's a valid name
566
- elif v in enum_cls.__members__:
567
- converted.append(enum_cls.__members__[v])
568
- else:
569
- log(
570
- f"[{cls.__name__}] Skipping invalid value for '{field}': '{v}' not in {enum_cls.__name__}",
571
- level=logging.WARNING,
572
- )
573
- clean_data[field] = converted
574
-
575
- # Single enum value
576
- elif (
577
- isinstance(raw_value, str) and raw_value in enum_cls._value2member_map_
578
- ):
579
- clean_data[field] = enum_cls(raw_value)
580
-
581
- elif isinstance(raw_value, enum_cls):
582
- # already the correct type
583
- continue
584
-
585
- else:
586
- log(
587
- message=f"[{cls.__name__}] Invalid value for '{field}': '{raw_value}' not in {enum_cls.__name__}",
588
- level=logging.WARNING,
589
- )
590
- clean_data[field] = None
591
-
592
- return cls(**clean_data)
593
-
594
- @classmethod
595
- def from_tool_arguments(cls: type[T], arguments: str) -> T:
596
- """Parse tool call arguments which may not be valid JSON.
597
-
598
- The OpenAI API is expected to return well-formed JSON for tool arguments,
599
- but minor formatting issues (such as the use of single quotes) can occur.
600
- This helper first tries ``json.loads`` and falls back to
601
- ``ast.literal_eval`` for simple cases.
602
-
603
- Parameters
604
- ----------
605
- arguments
606
- Raw argument string from the tool call.
607
-
608
- Returns
609
- -------
610
- dict
611
- Parsed dictionary of arguments.
612
-
613
- Raises
614
- ------
615
- ValueError
616
- If the arguments cannot be parsed as JSON.
617
-
618
- Examples
619
- --------
620
- >>> parse_tool_arguments('{"key": "value"}')["key"]
621
- 'value'
622
- """
623
- try:
624
- structured_data = json.loads(arguments)
625
-
626
- except json.JSONDecodeError:
627
- try:
628
- structured_data = ast.literal_eval(arguments)
629
- except (SyntaxError, ValueError) as exc:
630
- raise ValueError(
631
- f"Invalid JSON arguments: {arguments}. "
632
- f"Expected valid JSON or Python literal."
633
- ) from exc
634
- return cls.from_raw_input(structured_data)
635
-
636
- @staticmethod
637
- def format_output(label: str, *, value: Any) -> str:
638
- """
639
- Format a label and value for string output.
640
-
641
- Handles None values and lists appropriately.
642
-
643
- Parameters
644
- ----------
645
- label : str
646
- Label describing the value.
647
- value : Any
648
- Value to format for display.
649
-
650
- Returns
651
- -------
652
- str
653
- Formatted string (for example ``"- Label: Value"``).
654
- """
655
- if not value:
656
- return f"- {label}: None"
657
- if isinstance(value, list):
658
- return f"- {label}: {', '.join(str(v) for v in value)}"
659
- return f"- {label}: {str(value)}"
660
-
661
604
  @classmethod
662
605
  def schema_overrides(cls) -> dict[str, Any]:
663
606
  """
@@ -686,14 +629,32 @@ class StructureBase(BaseModelJSONSerializable):
686
629
  ]
687
630
  )
688
631
 
689
- def console_print(self) -> None:
690
- """Output the result of :meth:`print` to stdout.
632
+ @classmethod
633
+ def from_dataclass(cls: type[T], data: Any) -> T:
634
+ """Create an instance from a dataclass object.
635
+
636
+ Parameters
637
+ ----------
638
+ data : Any
639
+ Dataclass instance, mapping, or object with attributes to convert.
640
+ Private attributes (prefixed with ``_``) are ignored.
691
641
 
692
642
  Returns
693
643
  -------
694
- None
644
+ T
645
+ New instance of the structure populated from the dataclass.
695
646
  """
696
- print(self.print())
647
+
648
+ def _filter_private(items: list[tuple[str, Any]]) -> dict[str, Any]:
649
+ return {name: value for name, value in items if not name.startswith("_")}
650
+
651
+ if dataclasses.is_dataclass(data) and not isinstance(data, type):
652
+ payload = dataclasses.asdict(data, dict_factory=_filter_private)
653
+ elif isinstance(data, Mapping):
654
+ payload = _filter_private(list(data.items()))
655
+ else:
656
+ payload = _filter_private(list(vars(data).items()))
657
+ return cls(**payload)
697
658
 
698
659
 
699
660
  @dataclass(frozen=True)