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.
- msgflux/__init__.py +104 -0
- msgflux/_private/__init__.py +0 -0
- msgflux/_private/client.py +105 -0
- msgflux/_private/core.py +14 -0
- msgflux/_private/executor.py +134 -0
- msgflux/_private/response.py +230 -0
- msgflux/auto/__init__.py +5 -0
- msgflux/auto/params.py +167 -0
- msgflux/cache.py +54 -0
- msgflux/core/__init__.py +5 -0
- msgflux/core/dotdict.py +264 -0
- msgflux/core/examples.py +169 -0
- msgflux/core/message.py +45 -0
- msgflux/data/__init__.py +0 -0
- msgflux/data/audio.py +1 -0
- msgflux/data/dbs/__init__.py +16 -0
- msgflux/data/dbs/base.py +124 -0
- msgflux/data/dbs/db.py +65 -0
- msgflux/data/dbs/providers/__init__.py +0 -0
- msgflux/data/dbs/providers/cachetools.py +57 -0
- msgflux/data/dbs/providers/diskcache.py +48 -0
- msgflux/data/dbs/providers/faiss.py +220 -0
- msgflux/data/dbs/registry.py +19 -0
- msgflux/data/dbs/response.py +10 -0
- msgflux/data/dbs/types.py +26 -0
- msgflux/data/parsers/__init__.py +16 -0
- msgflux/data/parsers/base.py +325 -0
- msgflux/data/parsers/parser.py +101 -0
- msgflux/data/parsers/providers/__init__.py +32 -0
- msgflux/data/parsers/providers/csv_parser.py +278 -0
- msgflux/data/parsers/providers/email_parser.py +264 -0
- msgflux/data/parsers/providers/html_parser.py +265 -0
- msgflux/data/parsers/providers/openpyxl.py +333 -0
- msgflux/data/parsers/providers/pypdf.py +186 -0
- msgflux/data/parsers/providers/python_docx.py +334 -0
- msgflux/data/parsers/providers/python_pptx.py +197 -0
- msgflux/data/parsers/registry.py +19 -0
- msgflux/data/parsers/response.py +32 -0
- msgflux/data/parsers/types.py +110 -0
- msgflux/data/retrievers/__init__.py +16 -0
- msgflux/data/retrievers/base.py +185 -0
- msgflux/data/retrievers/providers/__init__.py +0 -0
- msgflux/data/retrievers/providers/bm25.py +230 -0
- msgflux/data/retrievers/providers/bm25s.py +221 -0
- msgflux/data/retrievers/providers/rank_bm25.py +148 -0
- msgflux/data/retrievers/providers/rapidfuzz.py +125 -0
- msgflux/data/retrievers/providers/wikipedia.py +281 -0
- msgflux/data/retrievers/registry.py +19 -0
- msgflux/data/retrievers/retriever.py +83 -0
- msgflux/data/retrievers/types.py +24 -0
- msgflux/data/types/__init__.py +29 -0
- msgflux/data/types/audio.py +112 -0
- msgflux/data/types/base.py +70 -0
- msgflux/data/types/file.py +114 -0
- msgflux/data/types/image.py +134 -0
- msgflux/data/types/video.py +121 -0
- msgflux/dsl/__init__.py +0 -0
- msgflux/dsl/inline.py +484 -0
- msgflux/dsl/signature.py +393 -0
- msgflux/dsl/typed_parsers/__init__.py +19 -0
- msgflux/dsl/typed_parsers/base.py +24 -0
- msgflux/dsl/typed_parsers/providers/__init__.py +0 -0
- msgflux/dsl/typed_parsers/providers/xml.py +259 -0
- msgflux/dsl/typed_parsers/registry.py +14 -0
- msgflux/envs.py +100 -0
- msgflux/exceptions.py +84 -0
- msgflux/generation/__init__.py +0 -0
- msgflux/generation/control_flow.py +163 -0
- msgflux/generation/reasoning/__init__.py +5 -0
- msgflux/generation/reasoning/cot.py +7 -0
- msgflux/generation/reasoning/react.py +289 -0
- msgflux/generation/reasoning/self_consistency.py +16 -0
- msgflux/generation/templates.py +53 -0
- msgflux/logger.py +159 -0
- msgflux/models/__init__.py +16 -0
- msgflux/models/base.py +39 -0
- msgflux/models/cache.py +125 -0
- msgflux/models/gateway.py +466 -0
- msgflux/models/httpx.py +81 -0
- msgflux/models/model.py +122 -0
- msgflux/models/profiles/__init__.py +48 -0
- msgflux/models/profiles/base.py +122 -0
- msgflux/models/profiles/loader.py +264 -0
- msgflux/models/profiles/registry.py +207 -0
- msgflux/models/providers/__init__.py +0 -0
- msgflux/models/providers/cerebras.py +30 -0
- msgflux/models/providers/groq.py +52 -0
- msgflux/models/providers/imagerouter.py +33 -0
- msgflux/models/providers/jinaai.py +331 -0
- msgflux/models/providers/ollama.py +51 -0
- msgflux/models/providers/openai.py +1884 -0
- msgflux/models/providers/openrouter.py +64 -0
- msgflux/models/providers/replicate.py +218 -0
- msgflux/models/providers/sambanova.py +38 -0
- msgflux/models/providers/together.py +57 -0
- msgflux/models/providers/vllm.py +131 -0
- msgflux/models/registry.py +19 -0
- msgflux/models/response.py +26 -0
- msgflux/models/tool_call_agg.py +85 -0
- msgflux/models/types.py +196 -0
- msgflux/nn/__init__.py +57 -0
- msgflux/nn/functional.py +645 -0
- msgflux/nn/hooks/__init__.py +24 -0
- msgflux/nn/hooks/guard.py +109 -0
- msgflux/nn/hooks/hook.py +123 -0
- msgflux/nn/modules/__init__.py +68 -0
- msgflux/nn/modules/agent.py +1927 -0
- msgflux/nn/modules/container.py +672 -0
- msgflux/nn/modules/embedder.py +261 -0
- msgflux/nn/modules/generator.py +21 -0
- msgflux/nn/modules/mediamaker.py +285 -0
- msgflux/nn/modules/module.py +2106 -0
- msgflux/nn/modules/predictor.py +238 -0
- msgflux/nn/modules/searcher.py +383 -0
- msgflux/nn/modules/speaker.py +222 -0
- msgflux/nn/modules/tool.py +839 -0
- msgflux/nn/modules/transcriber.py +273 -0
- msgflux/nn/parameter.py +77 -0
- msgflux/protocols/__init__.py +1 -0
- msgflux/protocols/mcp/__init__.py +61 -0
- msgflux/protocols/mcp/auth/__init__.py +17 -0
- msgflux/protocols/mcp/auth/apikey.py +70 -0
- msgflux/protocols/mcp/auth/base.py +83 -0
- msgflux/protocols/mcp/auth/basic.py +57 -0
- msgflux/protocols/mcp/auth/bearer.py +97 -0
- msgflux/protocols/mcp/auth/custom.py +76 -0
- msgflux/protocols/mcp/auth/oauth2.py +133 -0
- msgflux/protocols/mcp/client.py +454 -0
- msgflux/protocols/mcp/exceptions.py +25 -0
- msgflux/protocols/mcp/integration.py +95 -0
- msgflux/protocols/mcp/loglevels.py +16 -0
- msgflux/protocols/mcp/transports.py +435 -0
- msgflux/protocols/mcp/types.py +51 -0
- msgflux/py.typed +0 -0
- msgflux/telemetry/__init__.py +22 -0
- msgflux/telemetry/config.py +104 -0
- msgflux/telemetry/span.py +245 -0
- msgflux/tools/__init__.py +10 -0
- msgflux/tools/builtin/__init__.py +5 -0
- msgflux/tools/builtin/web_fetch.py +121 -0
- msgflux/tools/config.py +196 -0
- msgflux/tools/definitions.py +11 -0
- msgflux/utils/__init__.py +0 -0
- msgflux/utils/chat.py +682 -0
- msgflux/utils/common.py +36 -0
- msgflux/utils/console.py +55 -0
- msgflux/utils/convert.py +20 -0
- msgflux/utils/encode.py +192 -0
- msgflux/utils/html.py +171 -0
- msgflux/utils/imports.py +115 -0
- msgflux/utils/inspect.py +81 -0
- msgflux/utils/logging.py +15 -0
- msgflux/utils/mermaid.py +20 -0
- msgflux/utils/msgspec.py +1116 -0
- msgflux/utils/pooling.py +51 -0
- msgflux/utils/templates.py +51 -0
- msgflux/utils/tenacity.py +38 -0
- msgflux/utils/torch.py +7 -0
- msgflux/utils/validation.py +31 -0
- msgflux/utils/xml.py +7 -0
- msgflux/version.py +1 -0
- msgflux-0.1.0.dist-info/METADATA +170 -0
- msgflux-0.1.0.dist-info/RECORD +165 -0
- msgflux-0.1.0.dist-info/WHEEL +4 -0
- 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
|
msgflux/core/__init__.py
ADDED
msgflux/core/dotdict.py
ADDED
|
@@ -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())
|
msgflux/core/examples.py
ADDED
|
@@ -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)
|