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
msgflux/auto/params.py ADDED
@@ -0,0 +1,167 @@
1
+ """AutoParams metaclass for automatic parameter management."""
2
+
3
+ from types import FunctionType, MethodType
4
+ from typing import Any, Dict
5
+
6
+
7
+ class AutoParams(type):
8
+ """Metaclass that captures class attributes and uses them as default parameters.
9
+
10
+ This metaclass automatically captures all non-callable, non-special attributes
11
+ defined in a class and makes them available as default parameter values when
12
+ instantiating the class.
13
+
14
+ The captured parameters are stored in the class's _auto_params attribute and
15
+ are automatically merged with any parameters passed during instantiation.
16
+
17
+ Basic Example:
18
+ >>> from msgflux.auto import AutoParams
19
+ >>>
20
+ >>> class Model(metaclass=AutoParams):
21
+ ... def __init__(self, learning_rate, batch_size, epochs):
22
+ ... self.learning_rate = learning_rate
23
+ ... self.batch_size = batch_size
24
+ ... self.epochs = epochs
25
+ ...
26
+ >>> class MyModel(Model):
27
+ ... learning_rate = 0.001
28
+ ... batch_size = 32
29
+ ... epochs = 100
30
+ ...
31
+ >>> # Uses default values from class attributes
32
+ >>> m = MyModel()
33
+ >>> print(m.learning_rate) # 0.001
34
+ >>>
35
+ >>> # Partial override
36
+ >>> m2 = MyModel(learning_rate=0.01, batch_size=64)
37
+ >>> print(m2.learning_rate) # 0.01
38
+ >>> print(m2.epochs) # 100 (still uses default)
39
+
40
+ Docstring as Parameter:
41
+ Classes can configure AutoParams to use their docstring as a parameter value
42
+ by setting the _autoparams_use_docstring_for class attribute.
43
+
44
+ >>> class Agent(metaclass=AutoParams):
45
+ ... _autoparams_use_docstring_for = "description"
46
+ ... def __init__(self, name, description=None):
47
+ ... self.name = name
48
+ ... self.description = description
49
+ ...
50
+ >>> class MyAgent(Agent):
51
+ ... '''An agent that helps with coding tasks'''
52
+ ...
53
+ >>> agent = MyAgent()
54
+ >>> print(agent.description) # "An agent that helps with coding tasks"
55
+ >>>
56
+ >>> # Explicit attribute takes precedence over docstring
57
+ >>> class MyAgent2(Agent):
58
+ ... '''This is ignored'''
59
+ ... description = "Explicit description"
60
+ ...
61
+ >>> agent2 = MyAgent2()
62
+ >>> print(agent2.description) # "Explicit description"
63
+
64
+ Class Name as Parameter:
65
+ Classes can configure AutoParams to use the class name as a parameter value
66
+ by setting the _autoparams_use_classname_for class attribute.
67
+
68
+ >>> class Agent(metaclass=AutoParams):
69
+ ... _autoparams_use_classname_for = "name"
70
+ ... def __init__(self, name):
71
+ ... self.name = name
72
+ ...
73
+ >>> class SuperAgent(Agent):
74
+ ... pass
75
+ ...
76
+ >>> agent = SuperAgent()
77
+ >>> print(agent.name) # "SuperAgent"
78
+ >>>
79
+ >>> # Explicit attribute takes precedence over class name
80
+ >>> class CustomAgent(Agent):
81
+ ... name = "my_custom_agent"
82
+ ...
83
+ >>> agent2 = CustomAgent()
84
+ >>> print(agent2.name) # "my_custom_agent"
85
+ """
86
+
87
+ def __new__(mcls, name: str, bases: tuple, namespace: Dict[str, Any]):
88
+ """Create a new class with auto parameter management.
89
+
90
+ Args:
91
+ name: Name of the class being created
92
+ bases: Base classes
93
+ namespace: Class namespace dictionary
94
+
95
+ Returns:
96
+ The newly created class with _auto_params attribute
97
+ """
98
+ # Collect auto params from base classes
99
+ inherited_params = {}
100
+ docstring_param_name = None
101
+ classname_param_name = None
102
+ for base in bases:
103
+ if hasattr(base, "_auto_params"):
104
+ inherited_params.update(base._auto_params)
105
+ # Check if base class wants to use docstring as a parameter
106
+ if hasattr(base, "_autoparams_use_docstring_for"):
107
+ docstring_param_name = base._autoparams_use_docstring_for
108
+ # Check if base class wants to use class name as a parameter
109
+ if hasattr(base, "_autoparams_use_classname_for"):
110
+ classname_param_name = base._autoparams_use_classname_for
111
+
112
+ # Capture all non-callable and non-special attributes from this class
113
+ # Exclude properties, classmethods, staticmethods, and functions/methods
114
+ # Allow classes (types) and instances (even if callable) to be collected
115
+ class_params = {
116
+ k: v
117
+ for k, v in namespace.items()
118
+ if not k.startswith("_")
119
+ and not isinstance(
120
+ v, (FunctionType, MethodType, classmethod, staticmethod, property)
121
+ )
122
+ }
123
+
124
+ # Handle class name as parameter if configured
125
+ if classname_param_name and classname_param_name not in class_params:
126
+ # Use the class name being created
127
+ class_params[classname_param_name] = name
128
+
129
+ # Handle docstring as parameter if configured
130
+ if docstring_param_name and "__doc__" in namespace and namespace["__doc__"]:
131
+ # Only use docstring if the parameter isn't already defined
132
+ if docstring_param_name not in class_params:
133
+ class_params[docstring_param_name] = namespace["__doc__"]
134
+
135
+ # Merge inherited params with class params (class params take precedence)
136
+ all_params = {**inherited_params, **class_params}
137
+
138
+ # Store the merged params in the namespace
139
+ namespace["_auto_params"] = all_params
140
+
141
+ return super().__new__(mcls, name, bases, namespace)
142
+
143
+ def __call__(cls, *args, **kwargs):
144
+ """Instantiate the class with auto parameters.
145
+
146
+ Merges the class's _auto_params with the provided kwargs, with kwargs
147
+ taking precedence. This allows for partial or complete override of
148
+ default parameters.
149
+
150
+ Args:
151
+ *args: Positional arguments to pass to __init__
152
+ **kwargs: Keyword arguments to pass to __init__
153
+
154
+ Returns:
155
+ Instance of the class
156
+ """
157
+ # Start with auto params
158
+ all_kwargs = dict(cls._auto_params) if hasattr(cls, "_auto_params") else {}
159
+
160
+ # Override with user-provided kwargs
161
+ all_kwargs.update(kwargs)
162
+
163
+ # Instantiate normally
164
+ return super().__call__(*args, **all_kwargs)
165
+
166
+
167
+ __all__ = ["AutoParams"]
msgflux/cache.py ADDED
@@ -0,0 +1,54 @@
1
+ from functools import wraps
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from msgflux.core.message import Message
5
+
6
+ if TYPE_CHECKING:
7
+ from msgflux.data.dbs.base import BaseDB
8
+ from msgflux.models.types import TextEmbedderModel
9
+
10
+
11
+ def response_cache(
12
+ db: "BaseDB",
13
+ task: Optional[str] = None,
14
+ response_mode: Optional[str] = None,
15
+ model: Optional["TextEmbedderModel"] = None,
16
+ ):
17
+ def decorator(func):
18
+ @wraps(func)
19
+ def wrapper(msg):
20
+ if isinstance(msg, Message):
21
+ if task is None:
22
+ raise ValueError(
23
+ "`task` is required when input is a Message instance"
24
+ )
25
+ key = msg.get(task)
26
+ else:
27
+ key = msg
28
+
29
+ if model:
30
+ key = model(key)
31
+
32
+ cached_response = db(key)
33
+
34
+ if cached_response is not None:
35
+ if isinstance(msg, Message):
36
+ if response_mode is None:
37
+ raise ValueError(
38
+ "`response_mode` is required when "
39
+ "input is a Message instance"
40
+ )
41
+ msg.set(response_mode, cached_response)
42
+ return msg
43
+ return cached_response
44
+
45
+ response = func(msg)
46
+
47
+ if response is not None:
48
+ db.add(key, response)
49
+
50
+ return response
51
+
52
+ return wrapper
53
+
54
+ return decorator
@@ -0,0 +1,5 @@
1
+ from msgflux.core.dotdict import dotdict
2
+ from msgflux.core.examples import Example, ExampleCollection
3
+ from msgflux.core.message import Message
4
+
5
+ __all__ = ["Example", "ExampleCollection", "Message", "dotdict"]
@@ -0,0 +1,264 @@
1
+ from typing import Any, Iterable, Mapping, Optional, Tuple, Union
2
+
3
+ import msgspec
4
+
5
+
6
+ class dotdict(dict): # noqa: N801
7
+ """A dictionary with dot access and nested path support.
8
+
9
+ dotdict allows you to access and modify values as attributes (e.g., `obj.key`)
10
+ and also allows reading and writing nested paths using strings with dot separators
11
+ (e.g., `obj.get("user.profile.name")`).
12
+
13
+ Main features:
14
+ - Dot access (`obj.key`)
15
+ - Traditional square bracket access (`obj['key']`)
16
+ - Nested reading via `.get("a.b.c")`
17
+ - Nested writing via `.set("a.b.c", value)`
18
+ - Conversion to standard dict with `.to_dict()`
19
+ - Support for Msgspec serialization (`__json__`)
20
+ - Support for lists with path indices (e.g., `"items.0.name"`)
21
+ - Optional immutability (`frozen=True`)
22
+ - Hidden keys support (`hidden_keys=["key1", "key2"]`)
23
+
24
+ Hidden Keys:
25
+ The `hidden_keys` parameter marks keys as invisible to enumeration and
26
+ discovery. Hidden keys are excluded from `keys()`, `values()`, `items()`,
27
+ `__iter__`, `__contains__` (`in` operator), `to_dict()`, `to_json()`,
28
+ `__repr__`, and `__str__`.
29
+
30
+ Hidden keys are still accessible via direct access: dot notation
31
+ (`obj.api_key`), bracket notation (`obj['api_key']`), and `get()` paths
32
+ (`obj.get('api_key')`). This makes them suitable for sensitive data like
33
+ API keys or passwords that should not leak into logs, serialized output,
34
+ or iteration, while remaining readable when explicitly requested.
35
+
36
+ Example:
37
+ >>> d = dotdict(
38
+ ... {"api_key": "secret", "username": "john"},
39
+ ... hidden_keys=["api_key"]
40
+ ... )
41
+ >>> "api_key" in d # False
42
+ >>> list(d.keys()) # ["username"]
43
+ >>> d.to_dict() # {"username": "john"}
44
+ >>> d.api_key # "secret"
45
+ >>> d["api_key"] # "secret"
46
+ >>> d.get("api_key") # "secret"
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ initial_data: Optional[
52
+ Union[Mapping[str, Any], Iterable[Tuple[str, Any]]]
53
+ ] = None,
54
+ *,
55
+ frozen: Optional[bool] = False,
56
+ hidden_keys: Optional[list[str]] = None,
57
+ **kwargs,
58
+ ):
59
+ """Initializes an instance of dotdict.
60
+
61
+ Args:
62
+ initial_data:
63
+ Base dictionary to initialize data.
64
+ frozen:
65
+ If True, prevents changes after creation.
66
+ hidden_keys:
67
+ List of keys that should not be returned by get() method.
68
+ These keys will return None (or default) when accessed via get().
69
+ **kwargs:
70
+ Additional key=value pairs.
71
+
72
+ ::: example:
73
+ d = dotdict({"user": {"name": "Maria"}}, frozen=False)
74
+ print(d.user.name)
75
+ >> Maria
76
+
77
+ # With hidden keys
78
+ d = dotdict({"api_key": "secret", "name": "John"}, hidden_keys=["api_key"])
79
+ print(d.get("api_key")) # None
80
+ print("api_key" in d) # False
81
+ print(d.to_dict()) # {"name": "John"}
82
+ print(d.api_key) # "secret" (direct access)
83
+ """
84
+ if initial_data is None:
85
+ initial_data = {}
86
+ elif not isinstance(initial_data, dict):
87
+ initial_data = dict(initial_data)
88
+ self._frozen = frozen
89
+ self._hidden_keys = set(hidden_keys or [])
90
+ super().__init__()
91
+ for key, value in {**initial_data, **kwargs}.items():
92
+ super().__setitem__(key, self._wrap(value))
93
+
94
+ def __iter__(self):
95
+ hidden = getattr(self, "_hidden_keys", set())
96
+ for key in super().__iter__():
97
+ if key not in hidden:
98
+ yield key
99
+
100
+ def __contains__(self, key):
101
+ hidden = getattr(self, "_hidden_keys", set())
102
+ if key in hidden:
103
+ return False
104
+ return super().__contains__(key)
105
+
106
+ def keys(self):
107
+ hidden = getattr(self, "_hidden_keys", set())
108
+ return [k for k in super().keys() if k not in hidden]
109
+
110
+ def values(self):
111
+ hidden = getattr(self, "_hidden_keys", set())
112
+ return [v for k, v in super().items() if k not in hidden]
113
+
114
+ def items(self):
115
+ hidden = getattr(self, "_hidden_keys", set())
116
+ return [(k, v) for k, v in super().items() if k not in hidden]
117
+
118
+ def __getattr__(self, attr: str):
119
+ try:
120
+ return self[attr]
121
+ except KeyError as e:
122
+ raise AttributeError(f"`dotdict` object has no attribute '{attr}'") from e
123
+
124
+ def __setattr__(self, key: str, value: Any):
125
+ if key.startswith("_"):
126
+ super().__setattr__(key, value)
127
+ elif hasattr(self, "_frozen") and self._frozen:
128
+ raise AttributeError("Cannot modify frozen dotdict")
129
+ else:
130
+ self[key] = value
131
+
132
+ def __setitem__(self, key: str, value: Any):
133
+ if getattr(self, "_frozen", False):
134
+ raise AttributeError("Cannot modify frozen dotdict")
135
+ super().__setitem__(key, self._wrap(value))
136
+
137
+ def __delattr__(self, key: str):
138
+ if getattr(self, "_frozen", False):
139
+ raise AttributeError("Cannot delete from frozen dotdict")
140
+ try:
141
+ del self[key]
142
+ except KeyError as e:
143
+ raise AttributeError(f"`dotdict` object has no attribute `{key}`") from e
144
+
145
+ def _wrap(self, value: Any):
146
+ if isinstance(value, dict):
147
+ return dotdict(
148
+ value,
149
+ frozen=getattr(self, "_frozen", False),
150
+ hidden_keys=list(getattr(self, "_hidden_keys", set())),
151
+ )
152
+ elif isinstance(value, list):
153
+ return [self._wrap(item) for item in value]
154
+ return value
155
+
156
+ def get(self, path: str, default: Any = None) -> Any:
157
+ """Access nested values via dot path.
158
+
159
+ !!! example:
160
+ get('user.profile.age').
161
+ """
162
+ keys = path.split(".")
163
+ current = self
164
+ try:
165
+ for key in keys:
166
+ if isinstance(current, list):
167
+ key = int(key) # noqa: PLW2901
168
+ current = current[key]
169
+ return current
170
+ except (KeyError, IndexError, ValueError, TypeError):
171
+ return default
172
+
173
+ def set(self, path: str, value: Any):
174
+ """Set nested value via dot path.
175
+
176
+ !!! example
177
+
178
+ set('user.profile.age', 31).
179
+ """
180
+ if self._frozen:
181
+ raise AttributeError("Cannot modify frozen dotdict")
182
+
183
+ keys = path.split(".")
184
+ current = self
185
+ for i, key in enumerate(keys):
186
+ if isinstance(current, list):
187
+ key = int(key) # noqa: PLW2901
188
+
189
+ if i == len(keys) - 1:
190
+ if isinstance(current, list):
191
+ key_i = int(key)
192
+ current[key_i] = self._wrap(value)
193
+ else:
194
+ current[key] = self._wrap(value)
195
+ return
196
+
197
+ if isinstance(current, list):
198
+ key_i = int(key)
199
+ if key_i >= len(current) or not isinstance(
200
+ current[key_i], (dict, dotdict)
201
+ ):
202
+ current[key_i] = dotdict()
203
+ current = current[key_i]
204
+ else:
205
+ if key not in current or not isinstance(
206
+ current[key], (dict, dotdict, list)
207
+ ):
208
+ current[key] = dotdict()
209
+ current = current[key]
210
+
211
+ def update(self, *args, **kwargs):
212
+ """Extends dict.update to support nested keys while maintaining DotDict."""
213
+ if self._frozen:
214
+ raise AttributeError("Cannot modify frozen DotDict")
215
+
216
+ # Collects all key-value pairs
217
+ other = {}
218
+ if args:
219
+ if len(args) > 1:
220
+ raise TypeError(
221
+ f"update expected at most 1 arguments, given `{len(args)}`"
222
+ )
223
+ other.update(args[0])
224
+ other.update(kwargs)
225
+
226
+ for key, value in other.items():
227
+ # Nested key with dot?
228
+ if isinstance(key, str) and "." in key:
229
+ self.set(key, value)
230
+
231
+ # Value is dict and there is already a dotdict on that key?
232
+ # Merge recursively
233
+ elif (
234
+ isinstance(value, dict)
235
+ and key in self
236
+ and isinstance(self[key], dotdict)
237
+ ):
238
+ self[key].update(value)
239
+
240
+ # General case: normal assignment (call __setitem__ e wrap)
241
+ else:
242
+ self[key] = value
243
+
244
+ def to_dict(self):
245
+ def unwrap(value):
246
+ if isinstance(value, dotdict):
247
+ return {k: unwrap(v) for k, v in value.items()}
248
+ elif isinstance(value, list):
249
+ return [unwrap(item) for item in value]
250
+ return value
251
+
252
+ return unwrap(self)
253
+
254
+ def to_json(self) -> bytes:
255
+ """Returns a encoded-JSON."""
256
+ return msgspec.json.encode(self.to_dict())
257
+
258
+ def __repr__(self):
259
+ d = self.to_dict()
260
+ attrs_str = "\n".join(f" '{k}': {v!r}" for k, v in d.items())
261
+ return f"{self.__class__.__name__}({{\n{attrs_str}\n}})"
262
+
263
+ def __str__(self):
264
+ return str(self.to_dict())
@@ -0,0 +1,169 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, List, Mapping, Optional, Union
3
+
4
+ from msgflux.utils.xml import apply_xml_tags
5
+
6
+
7
+ @dataclass
8
+ class Example:
9
+ """Represents a single example for a LM.
10
+
11
+ Args:
12
+ inputs:
13
+ Task inputs.
14
+ labels:
15
+ Expected model output.
16
+ title:
17
+ Example title.
18
+ reasoning:
19
+ Intermediate thinking/logic before generating output.
20
+ topic:
21
+ Example topic/category.
22
+ """
23
+
24
+ inputs: Union[str, Mapping[str, Any]]
25
+ labels: Union[str, Mapping[str, Any]]
26
+ reasoning: Optional[str] = None
27
+ title: Optional[str] = None
28
+ topic: Optional[str] = None
29
+ _inputs_needs_transform: bool = field(init=False)
30
+ _labels_needs_transform: bool = field(init=False)
31
+
32
+ def __post_init__(self):
33
+ self._inputs_needs_transform = isinstance(self.inputs, dict)
34
+ self._labels_needs_transform = isinstance(self.labels, dict)
35
+
36
+ def transform(
37
+ self,
38
+ input_transformer: Optional[callable] = None,
39
+ output_transformer: Optional[callable] = None,
40
+ ) -> "Example":
41
+ """Returns a new Example instance with transformed inputs/outputs as strings.
42
+
43
+ Args:
44
+ input_transformer:
45
+ Function to transform input dict to string.
46
+ output_transformer:
47
+ Function to transform output dict to string.
48
+
49
+ Returns:
50
+ New Example instance with string values
51
+ """
52
+ new_input = self.inputs
53
+ new_output = self.labels
54
+
55
+ if self._inputs_needs_transform and input_transformer:
56
+ new_input = input_transformer(self.inputs)
57
+
58
+ if self._labels_needs_transform and output_transformer:
59
+ new_output = output_transformer(self.labels)
60
+
61
+ return Example(
62
+ inputs=new_input,
63
+ labels=new_output,
64
+ title=self.title,
65
+ reasoning=self.reasoning,
66
+ topic=self.topic,
67
+ )
68
+
69
+ def to_xml(self, example_id: int) -> str:
70
+ """Convert the example to XML format."""
71
+ attributes = [f"id={example_id}"]
72
+
73
+ if self.title:
74
+ attributes.append(f'title="{self.title}"')
75
+ if self.topic:
76
+ attributes.append(f'topic="{self.topic}"')
77
+
78
+ attr_str = " ".join(attributes)
79
+
80
+ result = [f"<example {attr_str}>"]
81
+ result.append(apply_xml_tags("input", str(self.inputs)))
82
+ if self.reasoning:
83
+ result.append(apply_xml_tags("reasoning", self.reasoning))
84
+ result.append(apply_xml_tags("output", str(self.labels)))
85
+ result.append("</example>")
86
+
87
+ return "\n".join(result)
88
+
89
+
90
+ class ExampleCollection:
91
+ def __init__(
92
+ self, examples: Optional[List[Union[Example, Mapping[str, Any]]]] = None
93
+ ):
94
+ self.examples = []
95
+ if examples:
96
+ for example in examples:
97
+ if isinstance(example, Example):
98
+ self.examples.append(example)
99
+ else:
100
+ self.add(**example)
101
+
102
+ def _add(self, example: Example):
103
+ self.examples.append(example)
104
+
105
+ def add(
106
+ self,
107
+ inputs: Union[str, Mapping[str, Any]],
108
+ labels: Union[str, Mapping[str, Any]],
109
+ title: Optional[str] = None,
110
+ reasoning: Optional[str] = None,
111
+ topic: Optional[str] = None,
112
+ ):
113
+ """Add an example to the collection."""
114
+ example = Example(
115
+ inputs=inputs,
116
+ labels=labels,
117
+ title=title,
118
+ reasoning=reasoning,
119
+ topic=topic,
120
+ )
121
+ self._add(example)
122
+
123
+ def get_examples(self) -> List[Example]:
124
+ """Get all examples in the collection."""
125
+ return self.examples
126
+
127
+ def filter_by_topic(self, topic: str) -> List[Example]:
128
+ """Filter examples by topic."""
129
+ return [ex for ex in self.examples if ex.topic == topic]
130
+
131
+ def get_examples_needing_transform(self) -> List[Example]:
132
+ """Returns examples that need transformation."""
133
+ return [
134
+ ex
135
+ for ex in self.examples
136
+ if ex._inputs_needs_transform or ex._labels_needs_transform
137
+ ]
138
+
139
+ def transform(
140
+ self,
141
+ input_transformer: Optional[callable] = None,
142
+ output_transformer: Optional[callable] = None,
143
+ ) -> "ExampleCollection":
144
+ """Transforms all examples to strings."""
145
+ transformed = [
146
+ ex.transform(input_transformer, output_transformer) for ex in self.examples
147
+ ]
148
+ return ExampleCollection(transformed)
149
+
150
+ def get_formatted(
151
+ self,
152
+ input_transformer: Optional[callable] = None,
153
+ output_transformer: Optional[callable] = None,
154
+ ) -> str:
155
+ """Converts the entire collection to XML format.
156
+
157
+ Returns:
158
+ Formatted examples using xml.
159
+ """
160
+ if not self.examples:
161
+ return None
162
+
163
+ collection = self.transform(input_transformer, output_transformer)
164
+
165
+ xml_parts = []
166
+ for i, example in enumerate(collection.get_examples(), start=1):
167
+ xml_parts.append(example.to_xml(i))
168
+
169
+ return "\n\n".join(xml_parts)