msgflux 0.1.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 (165) hide show
  1. msgflux/__init__.py +104 -0
  2. msgflux/_private/__init__.py +0 -0
  3. msgflux/_private/client.py +105 -0
  4. msgflux/_private/core.py +14 -0
  5. msgflux/_private/executor.py +134 -0
  6. msgflux/_private/response.py +230 -0
  7. msgflux/auto/__init__.py +5 -0
  8. msgflux/auto/params.py +167 -0
  9. msgflux/cache.py +54 -0
  10. msgflux/core/__init__.py +5 -0
  11. msgflux/core/dotdict.py +264 -0
  12. msgflux/core/examples.py +169 -0
  13. msgflux/core/message.py +45 -0
  14. msgflux/data/__init__.py +0 -0
  15. msgflux/data/audio.py +1 -0
  16. msgflux/data/dbs/__init__.py +16 -0
  17. msgflux/data/dbs/base.py +124 -0
  18. msgflux/data/dbs/db.py +65 -0
  19. msgflux/data/dbs/providers/__init__.py +0 -0
  20. msgflux/data/dbs/providers/cachetools.py +57 -0
  21. msgflux/data/dbs/providers/diskcache.py +48 -0
  22. msgflux/data/dbs/providers/faiss.py +220 -0
  23. msgflux/data/dbs/registry.py +19 -0
  24. msgflux/data/dbs/response.py +10 -0
  25. msgflux/data/dbs/types.py +26 -0
  26. msgflux/data/parsers/__init__.py +16 -0
  27. msgflux/data/parsers/base.py +325 -0
  28. msgflux/data/parsers/parser.py +101 -0
  29. msgflux/data/parsers/providers/__init__.py +32 -0
  30. msgflux/data/parsers/providers/csv_parser.py +278 -0
  31. msgflux/data/parsers/providers/email_parser.py +264 -0
  32. msgflux/data/parsers/providers/html_parser.py +265 -0
  33. msgflux/data/parsers/providers/openpyxl.py +333 -0
  34. msgflux/data/parsers/providers/pypdf.py +186 -0
  35. msgflux/data/parsers/providers/python_docx.py +334 -0
  36. msgflux/data/parsers/providers/python_pptx.py +197 -0
  37. msgflux/data/parsers/registry.py +19 -0
  38. msgflux/data/parsers/response.py +32 -0
  39. msgflux/data/parsers/types.py +110 -0
  40. msgflux/data/retrievers/__init__.py +16 -0
  41. msgflux/data/retrievers/base.py +185 -0
  42. msgflux/data/retrievers/providers/__init__.py +0 -0
  43. msgflux/data/retrievers/providers/bm25.py +230 -0
  44. msgflux/data/retrievers/providers/bm25s.py +221 -0
  45. msgflux/data/retrievers/providers/rank_bm25.py +148 -0
  46. msgflux/data/retrievers/providers/rapidfuzz.py +125 -0
  47. msgflux/data/retrievers/providers/wikipedia.py +281 -0
  48. msgflux/data/retrievers/registry.py +19 -0
  49. msgflux/data/retrievers/retriever.py +83 -0
  50. msgflux/data/retrievers/types.py +24 -0
  51. msgflux/data/types/__init__.py +29 -0
  52. msgflux/data/types/audio.py +112 -0
  53. msgflux/data/types/base.py +70 -0
  54. msgflux/data/types/file.py +114 -0
  55. msgflux/data/types/image.py +134 -0
  56. msgflux/data/types/video.py +121 -0
  57. msgflux/dsl/__init__.py +0 -0
  58. msgflux/dsl/inline.py +484 -0
  59. msgflux/dsl/signature.py +393 -0
  60. msgflux/dsl/typed_parsers/__init__.py +19 -0
  61. msgflux/dsl/typed_parsers/base.py +24 -0
  62. msgflux/dsl/typed_parsers/providers/__init__.py +0 -0
  63. msgflux/dsl/typed_parsers/providers/xml.py +259 -0
  64. msgflux/dsl/typed_parsers/registry.py +14 -0
  65. msgflux/envs.py +100 -0
  66. msgflux/exceptions.py +84 -0
  67. msgflux/generation/__init__.py +0 -0
  68. msgflux/generation/control_flow.py +163 -0
  69. msgflux/generation/reasoning/__init__.py +5 -0
  70. msgflux/generation/reasoning/cot.py +7 -0
  71. msgflux/generation/reasoning/react.py +289 -0
  72. msgflux/generation/reasoning/self_consistency.py +16 -0
  73. msgflux/generation/templates.py +53 -0
  74. msgflux/logger.py +159 -0
  75. msgflux/models/__init__.py +16 -0
  76. msgflux/models/base.py +39 -0
  77. msgflux/models/cache.py +125 -0
  78. msgflux/models/gateway.py +466 -0
  79. msgflux/models/httpx.py +81 -0
  80. msgflux/models/model.py +122 -0
  81. msgflux/models/profiles/__init__.py +48 -0
  82. msgflux/models/profiles/base.py +122 -0
  83. msgflux/models/profiles/loader.py +264 -0
  84. msgflux/models/profiles/registry.py +207 -0
  85. msgflux/models/providers/__init__.py +0 -0
  86. msgflux/models/providers/cerebras.py +30 -0
  87. msgflux/models/providers/groq.py +52 -0
  88. msgflux/models/providers/imagerouter.py +33 -0
  89. msgflux/models/providers/jinaai.py +331 -0
  90. msgflux/models/providers/ollama.py +51 -0
  91. msgflux/models/providers/openai.py +1884 -0
  92. msgflux/models/providers/openrouter.py +64 -0
  93. msgflux/models/providers/replicate.py +218 -0
  94. msgflux/models/providers/sambanova.py +38 -0
  95. msgflux/models/providers/together.py +57 -0
  96. msgflux/models/providers/vllm.py +131 -0
  97. msgflux/models/registry.py +19 -0
  98. msgflux/models/response.py +26 -0
  99. msgflux/models/tool_call_agg.py +85 -0
  100. msgflux/models/types.py +196 -0
  101. msgflux/nn/__init__.py +57 -0
  102. msgflux/nn/functional.py +645 -0
  103. msgflux/nn/hooks/__init__.py +24 -0
  104. msgflux/nn/hooks/guard.py +109 -0
  105. msgflux/nn/hooks/hook.py +123 -0
  106. msgflux/nn/modules/__init__.py +68 -0
  107. msgflux/nn/modules/agent.py +1927 -0
  108. msgflux/nn/modules/container.py +672 -0
  109. msgflux/nn/modules/embedder.py +261 -0
  110. msgflux/nn/modules/generator.py +21 -0
  111. msgflux/nn/modules/mediamaker.py +285 -0
  112. msgflux/nn/modules/module.py +2106 -0
  113. msgflux/nn/modules/predictor.py +238 -0
  114. msgflux/nn/modules/searcher.py +383 -0
  115. msgflux/nn/modules/speaker.py +222 -0
  116. msgflux/nn/modules/tool.py +839 -0
  117. msgflux/nn/modules/transcriber.py +273 -0
  118. msgflux/nn/parameter.py +77 -0
  119. msgflux/protocols/__init__.py +1 -0
  120. msgflux/protocols/mcp/__init__.py +61 -0
  121. msgflux/protocols/mcp/auth/__init__.py +17 -0
  122. msgflux/protocols/mcp/auth/apikey.py +70 -0
  123. msgflux/protocols/mcp/auth/base.py +83 -0
  124. msgflux/protocols/mcp/auth/basic.py +57 -0
  125. msgflux/protocols/mcp/auth/bearer.py +97 -0
  126. msgflux/protocols/mcp/auth/custom.py +76 -0
  127. msgflux/protocols/mcp/auth/oauth2.py +133 -0
  128. msgflux/protocols/mcp/client.py +454 -0
  129. msgflux/protocols/mcp/exceptions.py +25 -0
  130. msgflux/protocols/mcp/integration.py +95 -0
  131. msgflux/protocols/mcp/loglevels.py +16 -0
  132. msgflux/protocols/mcp/transports.py +435 -0
  133. msgflux/protocols/mcp/types.py +51 -0
  134. msgflux/py.typed +0 -0
  135. msgflux/telemetry/__init__.py +22 -0
  136. msgflux/telemetry/config.py +104 -0
  137. msgflux/telemetry/span.py +245 -0
  138. msgflux/tools/__init__.py +10 -0
  139. msgflux/tools/builtin/__init__.py +5 -0
  140. msgflux/tools/builtin/web_fetch.py +121 -0
  141. msgflux/tools/config.py +196 -0
  142. msgflux/tools/definitions.py +11 -0
  143. msgflux/utils/__init__.py +0 -0
  144. msgflux/utils/chat.py +682 -0
  145. msgflux/utils/common.py +36 -0
  146. msgflux/utils/console.py +55 -0
  147. msgflux/utils/convert.py +20 -0
  148. msgflux/utils/encode.py +192 -0
  149. msgflux/utils/html.py +171 -0
  150. msgflux/utils/imports.py +115 -0
  151. msgflux/utils/inspect.py +81 -0
  152. msgflux/utils/logging.py +15 -0
  153. msgflux/utils/mermaid.py +20 -0
  154. msgflux/utils/msgspec.py +1116 -0
  155. msgflux/utils/pooling.py +51 -0
  156. msgflux/utils/templates.py +51 -0
  157. msgflux/utils/tenacity.py +38 -0
  158. msgflux/utils/torch.py +7 -0
  159. msgflux/utils/validation.py +31 -0
  160. msgflux/utils/xml.py +7 -0
  161. msgflux/version.py +1 -0
  162. msgflux-0.1.0.dist-info/METADATA +170 -0
  163. msgflux-0.1.0.dist-info/RECORD +165 -0
  164. msgflux-0.1.0.dist-info/WHEEL +4 -0
  165. msgflux-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1116 @@
1
+ import ast
2
+ import os
3
+ import re
4
+ from collections import OrderedDict
5
+ from collections.abc import Mapping as ABCMapping
6
+ from enum import Enum
7
+ from typing import (
8
+ Any,
9
+ Dict,
10
+ List,
11
+ Literal,
12
+ Mapping,
13
+ Optional,
14
+ Set,
15
+ Tuple,
16
+ Type,
17
+ Union,
18
+ get_args,
19
+ get_origin,
20
+ )
21
+
22
+ import msgspec
23
+ from msgspec import Meta, Struct, defstruct
24
+ from typing_extensions import Annotated
25
+
26
+ from msgflux.core.dotdict import dotdict
27
+ from msgflux.dsl.signature import FieldInfo
28
+ from msgflux.logger import logger
29
+ from msgflux.utils.common import type_mapping
30
+
31
+
32
+ class StructFactory:
33
+ """Rebuild msgspec.Struct from a JSON-schema or str signature."""
34
+
35
+ def __init__(self):
36
+ self.reconstructed_classes = OrderedDict()
37
+
38
+ @classmethod
39
+ def from_json_schema(cls, json_schema: Dict[str, Any]) -> msgspec.Struct:
40
+ self = cls() # Temp instance
41
+ if "$defs" not in json_schema:
42
+ raise ValueError("json_schema must contain definitions in `$defs`")
43
+
44
+ definitions = json_schema["$defs"]
45
+
46
+ dependency_order = self._get_dependency_order(definitions)
47
+
48
+ for class_name in dependency_order:
49
+ class_def = definitions[class_name]
50
+ self._reconstruct_class(class_name, class_def, definitions)
51
+
52
+ main_struct = self.reconstructed_classes.popitem(last=True)[1]
53
+ return main_struct
54
+
55
+ def _get_dependency_order(self, definitions: Dict[str, Any]) -> List[str]:
56
+ dependencies = {}
57
+ for class_name, class_def in definitions.items():
58
+ deps = set()
59
+ self._find_dependencies(class_def, deps, definitions.keys())
60
+ dependencies[class_name] = deps
61
+
62
+ ordered = []
63
+ remaining = set(definitions.keys())
64
+
65
+ while remaining:
66
+ ready = [
67
+ name for name in remaining if dependencies[name].issubset(set(ordered))
68
+ ]
69
+ if not ready: # Circular dependency - uses alphabetical order as fallback
70
+ ready = [min(remaining)]
71
+ for name in ready:
72
+ ordered.append(name)
73
+ remaining.remove(name)
74
+
75
+ return ordered
76
+
77
+ def _find_dependencies(
78
+ self, definition: Dict[str, Any], deps: Set[str], available_classes: Set[str]
79
+ ):
80
+ if isinstance(definition, dict):
81
+ if "$ref" in definition:
82
+ ref_name = definition["$ref"].split("/")[-1]
83
+ if ref_name in available_classes:
84
+ deps.add(ref_name)
85
+
86
+ if "properties" in definition:
87
+ for prop_def in definition["properties"].values():
88
+ self._find_dependencies(prop_def, deps, available_classes)
89
+
90
+ if "anyOf" in definition:
91
+ for item in definition["anyOf"]:
92
+ self._find_dependencies(item, deps, available_classes)
93
+
94
+ if "items" in definition:
95
+ self._find_dependencies(definition["items"], deps, available_classes)
96
+
97
+ def _reconstruct_class(
98
+ self,
99
+ class_name: str,
100
+ class_def: Dict[str, Any],
101
+ all_definitions: Dict[str, Any],
102
+ ):
103
+ if class_name in self.reconstructed_classes:
104
+ return self.reconstructed_classes[class_name]
105
+
106
+ properties = class_def.get("properties", {})
107
+ required_fields = set(class_def.get("required", []))
108
+
109
+ fields = []
110
+
111
+ for field_name, field_def in properties.items():
112
+ field_type = self._resolve_field_type(field_def, all_definitions)
113
+
114
+ if field_name in required_fields:
115
+ fields.append((field_name, field_type))
116
+ else:
117
+ fields.append((field_name, field_type, None))
118
+
119
+ reconstructed_class = defstruct(
120
+ class_name,
121
+ fields,
122
+ kw_only=True,
123
+ module=__name__,
124
+ )
125
+
126
+ self.reconstructed_classes[class_name] = reconstructed_class
127
+ return reconstructed_class
128
+
129
+ def _resolve_field_type(
130
+ self, field_def: Dict[str, Any], all_definitions: Dict[str, Any]
131
+ ):
132
+ if "anyOf" in field_def:
133
+ types = []
134
+ description = None
135
+
136
+ for option in field_def["anyOf"]:
137
+ if option.get("type") == "null":
138
+ continue
139
+
140
+ if "description" in option:
141
+ description = option["description"]
142
+
143
+ option_type = self._resolve_single_type(option, all_definitions)
144
+ if option_type and option_type is not type(None):
145
+ types.append(option_type)
146
+
147
+ if len(types) == 1:
148
+ base_type = types[0]
149
+ if description:
150
+ annotated_type = Annotated[base_type, Meta(description=description)]
151
+ return Optional[annotated_type]
152
+ else:
153
+ return Optional[base_type]
154
+ elif len(types) > 1:
155
+ if description:
156
+ union_type = Union[tuple(types)]
157
+ annotated_type = Annotated[
158
+ union_type, Meta(description=description)
159
+ ]
160
+ return Optional[annotated_type]
161
+ else:
162
+ return Optional[Union[tuple(types)]]
163
+ else:
164
+ return Optional[str]
165
+
166
+ return self._resolve_single_type(field_def, all_definitions)
167
+
168
+ def _resolve_single_type(
169
+ self, type_def: Dict[str, Any], all_definitions: Dict[str, Any]
170
+ ):
171
+ if "$ref" in type_def:
172
+ ref_name = type_def["$ref"].split("/")[-1]
173
+ if ref_name in self.reconstructed_classes:
174
+ return self.reconstructed_classes[ref_name]
175
+ else:
176
+ return ref_name
177
+
178
+ if "enum" in type_def:
179
+ enum_values = type_def["enum"]
180
+ description = type_def.get("description")
181
+
182
+ if len(enum_values) > 0:
183
+ if len(enum_values) == 1:
184
+ literal_type = Literal[enum_values[0]]
185
+ else:
186
+ literal_type = Literal[tuple(enum_values)]
187
+
188
+ if description:
189
+ return Annotated[literal_type, Meta(description=description)]
190
+ else:
191
+ return literal_type
192
+
193
+ if "type" in type_def:
194
+ type_name = type_def["type"]
195
+
196
+ if type_name == "array":
197
+ if "items" in type_def:
198
+ item_type = self._resolve_single_type(
199
+ type_def["items"], all_definitions
200
+ )
201
+ return List[item_type]
202
+ else:
203
+ return List[Any]
204
+
205
+ return type_mapping.get(type_name, str)
206
+
207
+ return str
208
+
209
+ @classmethod
210
+ def from_signature(
211
+ cls,
212
+ signature: str,
213
+ struct_name: Optional[str] = "DynamicStruct",
214
+ field_descriptions: Optional[Dict[str, str]] = None,
215
+ ) -> type:
216
+ annotations = cls._parse_annotations(signature)
217
+ struct_fields = []
218
+
219
+ for info in annotations:
220
+ try:
221
+ parsed_type = cls._parse_type_string(info.dtype)
222
+
223
+ if field_descriptions and info.name in field_descriptions:
224
+ annotated_type = Annotated[
225
+ parsed_type, Meta(description=field_descriptions[info.name])
226
+ ]
227
+ struct_fields.append((info.name, annotated_type))
228
+ else:
229
+ struct_fields.append((info.name, parsed_type))
230
+
231
+ except ValueError as e:
232
+ raise ValueError(
233
+ f"Error parsing field `{info.name}` (type='{info.dtype}')"
234
+ ) from e
235
+ except Exception as e:
236
+ raise RuntimeError(
237
+ f"Unexpected error parsing field `{info.name}`"
238
+ ) from e
239
+
240
+ if not struct_fields and signature.strip():
241
+ raise ValueError("No valid fields parsed from the signature.")
242
+
243
+ try:
244
+ DynamicStruct = msgspec.defstruct(struct_name, struct_fields) # noqa: N806
245
+ except Exception as e:
246
+ raise RuntimeError(f"Error creating struct `{struct_name}`") from e
247
+
248
+ return DynamicStruct
249
+
250
+ @classmethod
251
+ def _parse_literal_args(cls, args_str: str) -> Tuple:
252
+ """Parse arguments inside Literal[...] robustly, respecting quotes
253
+ and nested structures.
254
+ """
255
+ try:
256
+ # split respecting nested brackets and quotes
257
+ parts = cls._split_args(args_str)
258
+ values = []
259
+ for p in parts:
260
+ try:
261
+ values.append(ast.literal_eval(p))
262
+ except Exception:
263
+ # fallback: an unquoted identifier - treat as string
264
+ if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", p):
265
+ values.append(p)
266
+ else:
267
+ raise
268
+ return tuple(values)
269
+ except (SyntaxError, ValueError, TypeError) as e:
270
+ logger.error(str(e))
271
+ raise ValueError(f"Invalid literal arguments: `{args_str}`") from e
272
+
273
+ @classmethod
274
+ def _split_args(cls, args_str: str) -> list[str]:
275
+ """Split by commas at top-level only. Respects nested brackets and quotes.
276
+
277
+ This is an improved version of your previous _split_args but keeps the same
278
+ method name to remain compatible with existing calls.
279
+ """
280
+ args = []
281
+ level = 0
282
+ current_arg_start = 0
283
+ in_quotes = None
284
+
285
+ if not args_str.strip():
286
+ return []
287
+
288
+ for i, char in enumerate(args_str):
289
+ if char in ("[", "{", "(") and not in_quotes:
290
+ level += 1
291
+ elif char in ("]", "}", ")") and not in_quotes:
292
+ level -= 1
293
+ if level < 0:
294
+ raise ValueError("Unbalanced brackets in type arguments")
295
+ elif char in ("'", '"'):
296
+ if in_quotes == char:
297
+ in_quotes = None
298
+ elif in_quotes is None:
299
+ in_quotes = char
300
+ elif char == "," and level == 0 and not in_quotes:
301
+ args.append(args_str[current_arg_start:i].strip())
302
+ current_arg_start = i + 1
303
+
304
+ args.append(args_str[current_arg_start:].strip())
305
+ return [arg for arg in args if arg]
306
+
307
+ @classmethod
308
+ def _parse_type_string(cls, type_str: str) -> type: # noqa: C901
309
+ """Recursive parser for nested generics like 'list[dict[str, int]]'.
310
+
311
+ Supported base names (case-insensitive): list, dict, tuple, union, optional,
312
+ literal, str, int, float, bool, any, none.
313
+ """
314
+ type_str = type_str.strip()
315
+ if not type_str:
316
+ raise ValueError("The type string cannot be empty.")
317
+
318
+ # map textual base names to typing objects
319
+ generic_bases = {
320
+ "list": List,
321
+ "dict": Dict,
322
+ "tuple": Tuple,
323
+ "union": Union,
324
+ "optional": Optional,
325
+ "literal": Literal,
326
+ "any": Any,
327
+ "str": str,
328
+ "int": int,
329
+ "float": float,
330
+ "bool": bool,
331
+ "none": type(None),
332
+ }
333
+
334
+ # simple (non-generic) form
335
+ if "[" not in type_str:
336
+ key = type_str.lower()
337
+ if key in generic_bases:
338
+ return generic_bases[key]
339
+ # fallback to global type_mapping if it exists
340
+ if key in globals().get("type_mapping", {}):
341
+ return globals()["type_mapping"][key]
342
+ raise ValueError(f"Unsupported or unknown type: `{type_str}`")
343
+
344
+ # match Base[arg,...]
345
+ m = re.match(r"^\s*([^\[\]]+)\s*\[(.*)\]\s*$", type_str, flags=re.DOTALL)
346
+ if not m:
347
+ raise ValueError(f"Malformed generic type: `{type_str}`")
348
+ base_name, inner = m.groups()
349
+ base_key = base_name.strip().lower()
350
+
351
+ if base_key not in generic_bases:
352
+ # try to resolve via type_mapping
353
+ if base_key in globals().get("type_mapping", {}):
354
+ base = globals()["type_mapping"][base_key]
355
+ else:
356
+ raise ValueError(
357
+ f"Base type not supported: `{base_name}` in `{type_str}`"
358
+ )
359
+ else:
360
+ base = generic_bases[base_key]
361
+
362
+ # Literal[...] special handling
363
+ if base is Literal:
364
+ lit_args = cls._parse_literal_args(inner)
365
+ if len(lit_args) == 1:
366
+ return Literal[lit_args[0]]
367
+ return Literal[lit_args]
368
+
369
+ # Optional[T] -> Union[T, NoneType]
370
+ if base is Optional:
371
+ parts = cls._split_args(inner)
372
+ if len(parts) != 1:
373
+ raise ValueError("Optional[...] requires exactly 1 argument")
374
+ inner_t = cls._parse_type_string(parts[0])
375
+ return Union[inner_t, type(None)]
376
+
377
+ # Union[...] -> Union[T1, T2, ...]
378
+ if base is Union:
379
+ parts = cls._split_args(inner)
380
+ if not parts:
381
+ raise ValueError("Union[...] cannot be empty")
382
+ parsed = tuple(cls._parse_type_string(p) for p in parts)
383
+ if len(parsed) == 1:
384
+ return parsed[0]
385
+ return Union[parsed]
386
+
387
+ # Tuple[...] handling
388
+ if base is Tuple:
389
+ parts = cls._split_args(inner)
390
+ if len(parts) == 1 and parts[0].endswith("..."):
391
+ item_type = cls._parse_type_string(parts[0][:-3].strip())
392
+ return Tuple[item_type, ...]
393
+ if not parts:
394
+ return Tuple[()]
395
+ parsed = tuple(cls._parse_type_string(p) for p in parts)
396
+ return Tuple[parsed]
397
+
398
+ # generic with positional args: List[T], Dict[K, V], etc.
399
+ parts = cls._split_args(inner)
400
+ parsed_parts = tuple(cls._parse_type_string(p) for p in parts) if parts else ()
401
+
402
+ if base is List:
403
+ if len(parsed_parts) != 1:
404
+ raise ValueError("List requires exactly 1 argument")
405
+ return List[parsed_parts[0]]
406
+ if base is Dict:
407
+ if len(parsed_parts) != 2:
408
+ raise ValueError("Dict requires exactly 2 arguments")
409
+ return Dict[parsed_parts[0], parsed_parts[1]]
410
+
411
+ # Attempt to parameterize any other generic base
412
+ try:
413
+ if parsed_parts:
414
+ return (
415
+ base[parsed_parts]
416
+ if len(parsed_parts) > 1
417
+ else base[parsed_parts[0]]
418
+ )
419
+ except Exception as e:
420
+ raise ValueError(
421
+ f"Could not create parameterized type for `{type_str}`: {e}"
422
+ ) from e
423
+
424
+ raise ValueError(f"Malformed or unsupported generic: `{type_str}`")
425
+
426
+ @classmethod
427
+ def _parse_annotations(cls, signature: str) -> List[Any]: # noqa: C901
428
+ fields = []
429
+ current_pos = 0
430
+ level = 0
431
+ in_quotes = None
432
+ current_field_start = 0
433
+ signature = signature.strip()
434
+
435
+ if not signature:
436
+ return []
437
+
438
+ while current_pos < len(signature):
439
+ char = signature[current_pos]
440
+ if char in ("[", "{", "(") and not in_quotes:
441
+ level += 1
442
+ elif char in ("]", "}", ")") and not in_quotes:
443
+ level -= 1
444
+ if level < 0:
445
+ raise ValueError(
446
+ "Unbalanced nesting near `{signature[current_pos:]}`"
447
+ )
448
+ elif char in ("'", '"'):
449
+ if in_quotes == char:
450
+ in_quotes = None
451
+ elif in_quotes is None:
452
+ in_quotes = char
453
+
454
+ if char == "," and level == 0 and not in_quotes:
455
+ field_str = signature[current_field_start:current_pos].strip()
456
+ if field_str:
457
+ fields.append(field_str)
458
+ current_field_start = current_pos + 1
459
+ current_pos += 1
460
+
461
+ if level != 0:
462
+ raise ValueError("Unbalanced brackets/parentheses in signature.")
463
+ if in_quotes:
464
+ raise ValueError("Unclosed quotation marks in signature.")
465
+
466
+ last_field_str = signature[current_field_start:].strip()
467
+ if last_field_str:
468
+ fields.append(last_field_str)
469
+
470
+ result: List[Any] = []
471
+ for field_str in fields:
472
+ parts = field_str.split(":", 1)
473
+ if len(parts) == 2:
474
+ key = parts[0].strip()
475
+ value_dtype = parts[1].strip()
476
+ if not key:
477
+ raise ValueError(f"Field name cannot be empty in `{field_str}`")
478
+ if not value_dtype:
479
+ raise ValueError(f"Type cannot be empty after ':' in `{field_str}`")
480
+ else:
481
+ key = field_str.strip()
482
+ value_dtype = "str"
483
+ if not key:
484
+ raise ValueError(f"Field name cannot be empty in `{field_str}`")
485
+
486
+ # FieldInfo is expected to be a simple struct/dataclass with (name, dtype)
487
+ result.append(FieldInfo(name=key, dtype=value_dtype))
488
+
489
+ return result
490
+
491
+
492
+ def msgspec_dumps(obj: object) -> str:
493
+ return msgspec.json.encode(obj).decode("utf-8")
494
+
495
+
496
+ def export_to_json(
497
+ obj: object, filepath: Union[str, os.PathLike], indent: Optional[int] = 4
498
+ ):
499
+ with open(filepath, "wb") as f:
500
+ obj_b = msgspec.json.encode(obj)
501
+ formatted_obj_b = msgspec.json.format(obj_b, indent=indent)
502
+ f.write(formatted_obj_b)
503
+
504
+
505
+ def save(obj: object, f: Union[str, os.PathLike]):
506
+ """Save a Python object to a file in either JSON format.
507
+
508
+ Args:
509
+ data:
510
+ Saved object.
511
+ filepath:
512
+ A string or os.PathLike object containing a file name.
513
+
514
+ Raises:
515
+ ValueError:
516
+ If the file format is not "json".
517
+ FileNotFoundError:
518
+ If the directory of the provided filepath does not exist.
519
+
520
+ !!! example
521
+ ``` python
522
+ data = {"name": "Satoshi", "age": 42}
523
+ save(data, "output.json")
524
+ ```
525
+ """
526
+ directory = os.path.dirname(f)
527
+ if directory and not os.path.exists(directory):
528
+ raise FileNotFoundError(f"The directory `{directory}` does not exist")
529
+
530
+ if f.endswith("json"):
531
+ export_to_json(obj, f)
532
+ else:
533
+ raise ValueError(f"Unsupported format: `{f}`. Use `json`.")
534
+
535
+
536
+ def read_json(filepath: Union[str, os.PathLike]) -> Mapping[str, Any]:
537
+ with open(filepath, "rb") as f:
538
+ return msgspec.json.decode(f.read())
539
+
540
+
541
+ def load(f: Union[str, os.PathLike]) -> Any:
542
+ """Load data from a file in either JSON.
543
+
544
+ Args:
545
+ f: A string or os.PathLike object containing a file name.
546
+
547
+ Returns:
548
+ The Python object loaded from the file.
549
+
550
+ Raises:
551
+ FileNotFoundError:
552
+ If the file does not exist.
553
+ ValueError:
554
+ If the file extension is not ".json".
555
+
556
+ !!! example
557
+ ``` python
558
+ data = load("data.json")
559
+ ```
560
+ """
561
+ if not os.path.exists(f):
562
+ raise FileNotFoundError(f"The file `{f}` does not exist.")
563
+
564
+ if f.endswith(".json"):
565
+ return read_json(f)
566
+ else:
567
+ raise ValueError(f"Unsupported file extension: `{f}`. Use `.json`")
568
+
569
+
570
+ def struct_to_dict(obj: object):
571
+ """Recursively converts a msgspec.Struct object to a pure Python dictionary."""
572
+ if isinstance(obj, msgspec.Struct):
573
+ # Convert the struct to a dictionary and recursively process each value
574
+ return {k: struct_to_dict(v) for k, v in msgspec.structs.asdict(obj).items()}
575
+ elif isinstance(obj, list):
576
+ # Convert each item in the list recursively
577
+ return [struct_to_dict(item) for item in obj]
578
+ elif isinstance(obj, dict):
579
+ # If it is a dictionary, recursively convert its values
580
+ return dotdict({k: struct_to_dict(v) for k, v in obj.items()})
581
+ else: # Returns the value as is for simple types
582
+ return obj
583
+
584
+
585
+ def lower_msgspec_struct_for_openai( # noqa: C901
586
+ struct_class: Type[Struct],
587
+ ) -> Type[Struct]:
588
+ """Build an OpenAI-compatible transport struct from a logical msgspec schema.
589
+
590
+ OpenAI Structured Outputs reject open-ended JSON objects such as
591
+ ``dict[str, T]``. This helper lowers those types to a closed object shape
592
+ using ``entries: list[{key, value}]`` while keeping the logical schema
593
+ untouched for the rest of msgflux.
594
+ """
595
+
596
+ def _is_struct_type(type_hint: Any) -> bool:
597
+ return isinstance(type_hint, type) and issubclass(type_hint, msgspec.Struct)
598
+
599
+ def _to_camel_case(name: str) -> str:
600
+ sanitized = re.sub(r"[^0-9A-Za-z]+", "_", name).strip("_") or "Generated"
601
+ parts = [part for part in sanitized.split("_") if part]
602
+ camel = "".join(part[:1].upper() + part[1:] for part in parts) or "Generated"
603
+ if camel[0].isdigit():
604
+ camel = f"T{camel}"
605
+ return camel
606
+
607
+ def _wrap_annotated(base_type: Any, metadata: Tuple[Any, ...]) -> Any:
608
+ if not metadata:
609
+ return base_type
610
+ return Annotated.__class_getitem__((base_type, *metadata))
611
+
612
+ def _format_type_hint(type_hint: Any) -> str:
613
+ if isinstance(type_hint, type):
614
+ return type_hint.__name__
615
+ return str(type_hint).replace("typing.", "")
616
+
617
+ class _Compiler:
618
+ def __init__(self):
619
+ self.cache: Dict[Any, Any] = {}
620
+
621
+ def _unsupported(self, path: str, type_hint: Any, reason: str) -> None:
622
+ path_str = path or "<root>"
623
+ msg = (
624
+ "Unsupported OpenAI structured output type "
625
+ f"at `{path_str}`: `{_format_type_hint(type_hint)}`. {reason}"
626
+ )
627
+ raise TypeError(msg)
628
+
629
+ def _validate_dict_key_type(self, key_type: Any, path: str) -> None: # noqa: C901
630
+ origin = get_origin(key_type)
631
+
632
+ if origin is Annotated:
633
+ self._validate_dict_key_type(get_args(key_type)[0], path)
634
+ return
635
+
636
+ if key_type is Any:
637
+ self._unsupported(
638
+ path,
639
+ key_type,
640
+ "`Any` is too broad for OpenAI structured outputs.",
641
+ )
642
+
643
+ if key_type in {str, int, float, bool, type(None)}:
644
+ return
645
+
646
+ if isinstance(key_type, type) and issubclass(key_type, Enum):
647
+ return
648
+
649
+ if origin is Literal:
650
+ if all(
651
+ isinstance(item, (str, int, float, bool, type(None)))
652
+ for item in get_args(key_type)
653
+ ):
654
+ return
655
+ self._unsupported(
656
+ path,
657
+ key_type,
658
+ "Dict keys must resolve to hashable scalar values.",
659
+ )
660
+
661
+ if origin is Union:
662
+ non_none_args = [
663
+ arg for arg in get_args(key_type) if arg is not type(None)
664
+ ]
665
+ if len(non_none_args) != 1:
666
+ self._unsupported(
667
+ path,
668
+ key_type,
669
+ "Dict keys only support Optional[T] style unions.",
670
+ )
671
+ self._validate_dict_key_type(non_none_args[0], path)
672
+ return
673
+
674
+ if origin in (tuple, Tuple):
675
+ tuple_args = get_args(key_type)
676
+ if len(tuple_args) == 2 and tuple_args[1] is Ellipsis:
677
+ self._validate_dict_key_type(tuple_args[0], f"{path}[]")
678
+ return
679
+ for index, item_type in enumerate(tuple_args):
680
+ self._validate_dict_key_type(item_type, f"{path}[{index}]")
681
+ return
682
+
683
+ self._unsupported(
684
+ path,
685
+ key_type,
686
+ "Dict keys must be hashable scalar, Literal, Enum, or Tuple values.",
687
+ )
688
+
689
+ def lower_struct(
690
+ self, schema_type: Type[Struct], name_hint: str, path_hint: str = ""
691
+ ) -> Type[Struct]:
692
+ cache_key = ("struct", schema_type)
693
+ cached = self.cache.get(cache_key)
694
+ if cached is not None:
695
+ return cached
696
+
697
+ fields = []
698
+ changed = False
699
+
700
+ for field in msgspec.structs.fields(schema_type):
701
+ field_path = f"{path_hint}.{field.name}" if path_hint else field.name
702
+ lowered_type = self.lower_type(
703
+ field.type, f"{name_hint}_{field.name}", field_path
704
+ )
705
+ changed = changed or lowered_type is not field.type
706
+
707
+ if field.default_factory is not msgspec.NODEFAULT:
708
+ fields.append(
709
+ (
710
+ field.name,
711
+ lowered_type,
712
+ msgspec.field(default_factory=field.default_factory),
713
+ )
714
+ )
715
+ elif field.default is not msgspec.NODEFAULT:
716
+ fields.append((field.name, lowered_type, field.default))
717
+ else:
718
+ fields.append((field.name, lowered_type))
719
+
720
+ if not changed:
721
+ self.cache[cache_key] = schema_type
722
+ return schema_type
723
+
724
+ lowered_struct = defstruct(
725
+ _to_camel_case(name_hint),
726
+ fields,
727
+ kw_only=True,
728
+ module=__name__,
729
+ )
730
+ self.cache[cache_key] = lowered_struct
731
+ return lowered_struct
732
+
733
+ def lower_type(self, type_hint: Any, name_hint: str, path: str) -> Any: # noqa: C901
734
+ origin = get_origin(type_hint)
735
+ metadata: Tuple[Any, ...] = ()
736
+ base_type = type_hint
737
+
738
+ if origin is Annotated:
739
+ args = get_args(type_hint)
740
+ base_type = args[0]
741
+ metadata = tuple(args[1:])
742
+ origin = get_origin(base_type)
743
+
744
+ if base_type is Any:
745
+ self._unsupported(
746
+ path,
747
+ base_type,
748
+ "`Any` is too broad for OpenAI structured outputs.",
749
+ )
750
+
751
+ if base_type in (dict, Dict):
752
+ self._unsupported(
753
+ path,
754
+ base_type,
755
+ "Use `Dict[K, V]` with explicit key and value types.",
756
+ )
757
+
758
+ if base_type in (list, List):
759
+ self._unsupported(
760
+ path,
761
+ base_type,
762
+ "Use `List[T]` with an explicit item type.",
763
+ )
764
+
765
+ if base_type in (tuple, Tuple):
766
+ self._unsupported(
767
+ path,
768
+ base_type,
769
+ "Use `Tuple[...]` with explicit item types.",
770
+ )
771
+
772
+ if base_type in (set, Set, frozenset):
773
+ self._unsupported(
774
+ path,
775
+ base_type,
776
+ "Sets are not supported by OpenAI structured outputs.",
777
+ )
778
+
779
+ if _is_struct_type(base_type):
780
+ lowered = self.lower_struct(
781
+ base_type, name_hint or base_type.__name__, path
782
+ )
783
+ return _wrap_annotated(lowered, metadata)
784
+
785
+ if origin in (list, List):
786
+ if len(get_args(base_type)) != 1:
787
+ self._unsupported(
788
+ path,
789
+ base_type,
790
+ "Lists require exactly one item type.",
791
+ )
792
+ (item_type,) = get_args(base_type)
793
+ lowered = List[
794
+ self.lower_type(item_type, f"{name_hint}_item", f"{path}[]")
795
+ ]
796
+ return _wrap_annotated(lowered, metadata)
797
+
798
+ if origin in (dict, Dict):
799
+ if len(get_args(base_type)) != 2:
800
+ self._unsupported(
801
+ path,
802
+ base_type,
803
+ "Dicts require explicit key and value types.",
804
+ )
805
+ key_type, value_type = get_args(base_type)
806
+ self._validate_dict_key_type(key_type, f"{path}.<key>")
807
+ lowered_key_type = self.lower_type(
808
+ key_type, f"{name_hint}_key", f"{path}.<key>"
809
+ )
810
+ lowered_value_type = self.lower_type(
811
+ value_type, f"{name_hint}_value", f"{path}.<value>"
812
+ )
813
+
814
+ entry_struct = defstruct(
815
+ f"{_to_camel_case(name_hint)}Entry",
816
+ [
817
+ ("key", lowered_key_type),
818
+ ("value", lowered_value_type),
819
+ ],
820
+ kw_only=True,
821
+ module=__name__,
822
+ )
823
+ lowered = defstruct(
824
+ f"{_to_camel_case(name_hint)}Map",
825
+ [("entries", List[entry_struct])],
826
+ kw_only=True,
827
+ module=__name__,
828
+ )
829
+ return _wrap_annotated(lowered, metadata)
830
+
831
+ if origin is Union:
832
+ non_none_args = [
833
+ arg for arg in get_args(base_type) if arg is not type(None)
834
+ ]
835
+ if len(non_none_args) > 1:
836
+ self._unsupported(
837
+ path,
838
+ base_type,
839
+ "Only Optional[T] unions are supported.",
840
+ )
841
+ lowered_args = tuple(
842
+ self.lower_type(arg, f"{name_hint}_option_{index}", path)
843
+ for index, arg in enumerate(get_args(base_type))
844
+ )
845
+ lowered = Union[lowered_args]
846
+ return _wrap_annotated(lowered, metadata)
847
+
848
+ if origin in (tuple, Tuple):
849
+ tuple_args = get_args(base_type)
850
+ if not tuple_args:
851
+ self._unsupported(
852
+ path,
853
+ base_type,
854
+ "Tuples require explicit item types.",
855
+ )
856
+ if len(tuple_args) == 2 and tuple_args[1] is Ellipsis:
857
+ lowered = Tuple[
858
+ self.lower_type(
859
+ tuple_args[0], f"{name_hint}_item", f"{path}[]"
860
+ ), # noqa: E501
861
+ ...,
862
+ ]
863
+ else:
864
+ lowered = Tuple[
865
+ tuple(
866
+ self.lower_type(
867
+ arg,
868
+ f"{name_hint}_item_{index}",
869
+ f"{path}[{index}]",
870
+ )
871
+ for index, arg in enumerate(tuple_args)
872
+ )
873
+ ]
874
+ return _wrap_annotated(lowered, metadata)
875
+
876
+ if origin in (set, Set, frozenset):
877
+ self._unsupported(
878
+ path,
879
+ base_type,
880
+ "Sets are not supported by OpenAI structured outputs.",
881
+ )
882
+
883
+ if origin in (Mapping, ABCMapping):
884
+ self._unsupported(
885
+ path,
886
+ base_type,
887
+ "Use `Dict[K, V]` instead of mapping abstractions.",
888
+ )
889
+
890
+ if origin is not None and origin is not Literal:
891
+ self._unsupported(
892
+ path,
893
+ base_type,
894
+ "This generic type is not supported by the OpenAI lowering layer.",
895
+ )
896
+
897
+ return _wrap_annotated(base_type, metadata)
898
+
899
+ if not issubclass(struct_class, msgspec.Struct):
900
+ raise TypeError(
901
+ "`struct_class` must be a `msgspec.Struct` subclass "
902
+ f"given `{type(struct_class)}`"
903
+ )
904
+
905
+ compiler = _Compiler()
906
+ return compiler.lower_struct(struct_class, struct_class.__name__)
907
+
908
+
909
+ def restore_transport_value( # noqa: C901
910
+ value: Any,
911
+ logical_type: Any,
912
+ *,
913
+ dict_factory: Type[dict] = dotdict,
914
+ strict: bool = False,
915
+ ) -> Any:
916
+ """Restore transport-lowered values using the original logical type hint.
917
+
918
+ The helper is intentionally reusable:
919
+ - ``strict=True`` is appropriate when restoring provider structured outputs.
920
+ - ``strict=False`` is appropriate when preparing tool kwargs, where values may
921
+ already be in their logical shape.
922
+ """
923
+ origin = get_origin(logical_type)
924
+
925
+ if origin is Annotated:
926
+ logical_type = get_args(logical_type)[0]
927
+ origin = get_origin(logical_type)
928
+
929
+ if value is None:
930
+ return None
931
+
932
+ if logical_type in (Any, object):
933
+ return value
934
+
935
+ if origin is Literal:
936
+ return value
937
+
938
+ if logical_type is bool:
939
+ if isinstance(value, bool):
940
+ return value
941
+ if value in (0, 1):
942
+ return bool(value)
943
+ if strict:
944
+ raise TypeError(f"Expected bool given `{type(value)}`")
945
+ return value
946
+
947
+ if logical_type in (str, int, float):
948
+ if isinstance(value, logical_type):
949
+ return value
950
+ try:
951
+ return logical_type(value)
952
+ except (TypeError, ValueError):
953
+ if strict:
954
+ raise
955
+ return value
956
+
957
+ if isinstance(logical_type, type) and issubclass(logical_type, Enum):
958
+ try:
959
+ return logical_type(value)
960
+ except ValueError:
961
+ if strict:
962
+ raise
963
+ return value
964
+
965
+ if isinstance(logical_type, type) and issubclass(logical_type, msgspec.Struct):
966
+ if not isinstance(value, Mapping):
967
+ if strict:
968
+ raise TypeError(
969
+ "Expected a mapping transport value for "
970
+ f"`{logical_type.__name__}` given `{type(value)}`"
971
+ )
972
+ return value
973
+ restored = {}
974
+ for field in msgspec.structs.fields(logical_type):
975
+ if field.name not in value:
976
+ continue
977
+ restored[field.name] = restore_transport_value(
978
+ value[field.name],
979
+ field.type,
980
+ dict_factory=dict_factory,
981
+ strict=strict,
982
+ )
983
+ return dotdict(restored)
984
+
985
+ if origin in (list, List):
986
+ if not isinstance(value, list):
987
+ if strict:
988
+ raise TypeError(
989
+ "Expected a list transport value for "
990
+ f"`{str(logical_type).replace('typing.', '')}` "
991
+ f"given `{type(value)}`"
992
+ )
993
+ return value
994
+ item_type = get_args(logical_type)[0]
995
+ return [
996
+ restore_transport_value(
997
+ item,
998
+ item_type,
999
+ dict_factory=dict_factory,
1000
+ strict=strict,
1001
+ )
1002
+ for item in value
1003
+ ]
1004
+
1005
+ if origin in (dict, Dict, Mapping, ABCMapping):
1006
+ args = get_args(logical_type)
1007
+ key_type, value_type = args if len(args) == 2 else (Any, Any)
1008
+ if not isinstance(value, Mapping):
1009
+ if strict:
1010
+ raise TypeError(
1011
+ "Expected a mapping transport value for "
1012
+ f"`{str(logical_type).replace('typing.', '')}` "
1013
+ f"given `{type(value)}`"
1014
+ )
1015
+ return value
1016
+
1017
+ if "entries" in value:
1018
+ items = value["entries"]
1019
+ elif strict:
1020
+ raise ValueError(
1021
+ "Expected transport mapping wrapper with required `entries` field "
1022
+ f"for `{str(logical_type).replace('typing.', '')}`"
1023
+ )
1024
+ else:
1025
+ items = [{"key": key, "value": item} for key, item in value.items()]
1026
+
1027
+ restored = {}
1028
+ for item in items:
1029
+ key = restore_transport_value(
1030
+ item["key"],
1031
+ key_type,
1032
+ dict_factory=dict_factory,
1033
+ strict=strict,
1034
+ )
1035
+ restored[key] = restore_transport_value(
1036
+ item["value"],
1037
+ value_type,
1038
+ dict_factory=dict_factory,
1039
+ strict=strict,
1040
+ )
1041
+ return dict_factory(restored)
1042
+
1043
+ if origin is Union:
1044
+ args = tuple(arg for arg in get_args(logical_type) if arg is not type(None))
1045
+ if len(args) == 1:
1046
+ return restore_transport_value(
1047
+ value,
1048
+ args[0],
1049
+ dict_factory=dict_factory,
1050
+ strict=strict,
1051
+ )
1052
+ if args and strict:
1053
+ raise TypeError(
1054
+ "Unsupported logical type during transport restoration: "
1055
+ f"`{str(logical_type).replace('typing.', '')}`. "
1056
+ "Only Optional[T] unions are supported."
1057
+ )
1058
+ return value
1059
+
1060
+ if origin in (tuple, Tuple):
1061
+ if not isinstance(value, (list, tuple)):
1062
+ if strict:
1063
+ raise TypeError(
1064
+ "Expected a tuple-compatible transport value for "
1065
+ f"`{str(logical_type).replace('typing.', '')}` "
1066
+ f"given `{type(value)}`"
1067
+ )
1068
+ return value
1069
+ tuple_args = get_args(logical_type)
1070
+ if len(tuple_args) == 2 and tuple_args[1] is Ellipsis:
1071
+ item_type = tuple_args[0]
1072
+ return tuple(
1073
+ restore_transport_value(
1074
+ item,
1075
+ item_type,
1076
+ dict_factory=dict_factory,
1077
+ strict=strict,
1078
+ )
1079
+ for item in value
1080
+ )
1081
+ return tuple(
1082
+ restore_transport_value(
1083
+ item,
1084
+ tuple_args[index],
1085
+ dict_factory=dict_factory,
1086
+ strict=strict,
1087
+ )
1088
+ for index, item in enumerate(value)
1089
+ )
1090
+
1091
+ return value
1092
+
1093
+
1094
+ def restore_openai_structured_output(value: Any, logical_type: Any) -> Any:
1095
+ """Restore provider-specific transport shapes to the logical output schema."""
1096
+ return restore_transport_value(
1097
+ value,
1098
+ logical_type,
1099
+ dict_factory=dotdict,
1100
+ strict=True,
1101
+ )
1102
+
1103
+
1104
+ def is_optional_field(struct_class: Type[Struct], field_name: str) -> bool:
1105
+ """Check if field is Optional."""
1106
+ field_type = struct_class.__annotations__.get(field_name)
1107
+
1108
+ if field_type is None:
1109
+ return False
1110
+
1111
+ origin = get_origin(field_type)
1112
+ if origin is Union or origin is Optional:
1113
+ args = get_args(field_type)
1114
+ return type(None) in args
1115
+
1116
+ return False