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/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ from importlib import import_module
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from msgspec_ext import load_dotenv
6
+
7
+ from msgflux.cache import response_cache
8
+ from msgflux.core.dotdict import dotdict
9
+ from msgflux.core.examples import Example
10
+ from msgflux.core.message import Message
11
+ from msgflux.data.dbs import DB
12
+ from msgflux.data.parsers import Parser
13
+ from msgflux.data.retrievers import Retriever
14
+ from msgflux.data.types import Audio, File, Image, Video
15
+ from msgflux.dsl.inline import Inline
16
+ from msgflux.dsl.signature import InputField, OutputField, Signature
17
+ from msgflux.envs import set_envs
18
+ from msgflux.exceptions import TaskError
19
+ from msgflux.models import Model
20
+ from msgflux.models.gateway import ModelGateway
21
+ from msgflux.telemetry import Spans
22
+ from msgflux.tools.config import tool_config
23
+ from msgflux.utils.chat import ChatBlock, ChatML
24
+ from msgflux.utils.console import cprint
25
+ from msgflux.utils.inspect import get_fn_name
26
+ from msgflux.utils.msgspec import load, msgspec_dumps, save
27
+
28
+ __all__ = [
29
+ "DB",
30
+ "Audio",
31
+ "ChatBlock",
32
+ "ChatML",
33
+ "Example",
34
+ "File",
35
+ "Image",
36
+ "Inline",
37
+ "InputField",
38
+ "Message",
39
+ "Model",
40
+ "ModelGateway",
41
+ "OutputField",
42
+ "Parser",
43
+ "Retriever",
44
+ "Signature",
45
+ "Spans",
46
+ "TaskError",
47
+ "Video",
48
+ "cprint",
49
+ "dotdict",
50
+ "get_fn_name",
51
+ "load",
52
+ "load_dotenv",
53
+ "msgspec_dumps",
54
+ "response_cache",
55
+ "save",
56
+ "set_envs",
57
+ "tool_config",
58
+ ]
59
+
60
+ _LAZY_IMPORTS = {
61
+ "Audio": ("msgflux.data.types", "Audio"),
62
+ "ChatBlock": ("msgflux.utils.chat", "ChatBlock"),
63
+ "ChatML": ("msgflux.utils.chat", "ChatML"),
64
+ "DB": ("msgflux.data.dbs", "DB"),
65
+ "Example": ("msgflux.core.examples", "Example"),
66
+ "File": ("msgflux.data.types", "File"),
67
+ "Image": ("msgflux.data.types", "Image"),
68
+ "Inline": ("msgflux.dsl.inline", "Inline"),
69
+ "InputField": ("msgflux.dsl.signature", "InputField"),
70
+ "Message": ("msgflux.core.message", "Message"),
71
+ "Model": ("msgflux.models", "Model"),
72
+ "ModelGateway": ("msgflux.models.gateway", "ModelGateway"),
73
+ "OutputField": ("msgflux.dsl.signature", "OutputField"),
74
+ "Parser": ("msgflux.data.parsers", "Parser"),
75
+ "Retriever": ("msgflux.data.retrievers", "Retriever"),
76
+ "Signature": ("msgflux.dsl.signature", "Signature"),
77
+ "Spans": ("msgflux.telemetry", "Spans"),
78
+ "TaskError": ("msgflux.exceptions", "TaskError"),
79
+ "Video": ("msgflux.data.types", "Video"),
80
+ "cprint": ("msgflux.utils.console", "cprint"),
81
+ "dotdict": ("msgflux.core.dotdict", "dotdict"),
82
+ "get_fn_name": ("msgflux.utils.inspect", "get_fn_name"),
83
+ "load": ("msgflux.utils.msgspec", "load"),
84
+ "load_dotenv": ("msgspec_ext", "load_dotenv"),
85
+ "msgspec_dumps": ("msgflux.utils.msgspec", "msgspec_dumps"),
86
+ "response_cache": ("msgflux.cache", "response_cache"),
87
+ "save": ("msgflux.utils.msgspec", "save"),
88
+ "set_envs": ("msgflux.envs", "set_envs"),
89
+ "tool_config": ("msgflux.tools.config", "tool_config"),
90
+ }
91
+
92
+
93
+ def __getattr__(name: str):
94
+ if name not in _LAZY_IMPORTS:
95
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
96
+
97
+ module_name, attr_name = _LAZY_IMPORTS[name]
98
+ value = getattr(import_module(module_name), attr_name)
99
+ globals()[name] = value
100
+ return value
101
+
102
+
103
+ def __dir__() -> list[str]:
104
+ return sorted(set(globals()) | set(__all__))
File without changes
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Dict
4
+
5
+ from msgflux._private.core import Core
6
+ from msgflux._private.executor import Executor
7
+
8
+
9
+ class BaseClient(ABC, Core):
10
+ """BaseClient is an abstract base class that defines the interface and
11
+ common behavior for client classes in the msgflux system.
12
+
13
+ This class inherits from `Core` to leverage its state management
14
+ capabilities and extends it with additional functionality specific to
15
+ clients. It requires subclasses to implement certain methods, ensuring
16
+ a consistent interface across different client implementations.
17
+
18
+ Subclasses must implement the `_initialize` and `__call__` methods,
19
+ which are essential for client initialization and operation,
20
+ respectively. Additionally, this class provides methods for
21
+ serialization and deserialization, allowing client instances to be
22
+ easily saved and restored.
23
+
24
+ Attributes:
25
+ to_ignore ([optional[list[str]]): A list of attribute keys to be
26
+ ignored during serialization.
27
+ msgflux_type (str): A string identifying the type of the client
28
+ in the msgflux system.
29
+ provider (optional[str]): The provider associated with the client,
30
+ if applicable.
31
+ instance_type (optional[callable]): A method or callable that
32
+ returns additional instance-specific data to be included during
33
+ serialization.
34
+ """
35
+
36
+ @abstractmethod
37
+ def __call__(self):
38
+ """Execute the client's main functionality. This method must be
39
+ implemented by subclasses.
40
+
41
+ This method defines the primary behavior of the client and is
42
+ called when the instance is invoked as a function.
43
+
44
+ Raises:
45
+ NotImplementedError: If the method is not implemented by the
46
+ subclass.
47
+ """
48
+ raise NotImplementedError
49
+
50
+ async def acall(self, *args, **kwargs):
51
+ """Async interface to __call__."""
52
+ executor = Executor.get_instance()
53
+ future = executor.submit(self.__call__, *args, **kwargs)
54
+ done, _ = await asyncio.wait([future])
55
+ response = await done[0]
56
+ return response
57
+
58
+ def serialize(self) -> Dict[str, Any]:
59
+ """Serialize the client instance into a dictionary.
60
+
61
+ This method prepares the client's state for serialization by
62
+ removing unnecessary attributes and including additional metadata
63
+ such as the client type and provider.
64
+
65
+ Returns:
66
+ Dict[str, Any]: A dictionary containing the serialized state
67
+ of the client, including metadata and the client's
68
+ internal state.
69
+ """
70
+ state = self.__getstate__()
71
+ if hasattr(self, "to_ignore"):
72
+ for key in self.to_ignore:
73
+ state.pop(key, None)
74
+ data = {"msgflux_type": self.msgflux_type}
75
+ if hasattr(self, "provider"):
76
+ data["provider"] = self.provider
77
+ if hasattr(self, "instance_type"):
78
+ instance_type = self.instance_type()
79
+ data.update(instance_type)
80
+ data["state"] = state
81
+ return data
82
+
83
+ @classmethod
84
+ def from_serialized(cls, state: Dict[str, Any]):
85
+ """Deserialize a client instance from a dictionary.
86
+
87
+ This method creates a new instance of the client and restores its
88
+ state from the provided dictionary. It also ensures that the
89
+ client is properly initialized after deserialization.
90
+
91
+ Args:
92
+ state (Dict[str, Any]): A dictionary containing the serialized
93
+ state of the client.
94
+
95
+ Returns:
96
+ BaseClient: A new instance of the client with its state
97
+ restored.
98
+ """
99
+ if "msgflux_type" in state:
100
+ state.pop("msgflux_type")
101
+ instance = cls()
102
+ instance.__setstate__(state)
103
+ if hasattr(instance, "_initialize"):
104
+ instance._initialize()
105
+ return instance
@@ -0,0 +1,14 @@
1
+ class Core:
2
+ """Core is a set of methods that are common among some classes in msgflux.
3
+
4
+ This class provides basic functionality for state management, allowing instances
5
+ to be serialized and deserialized. It is intended to be inherited by other classes
6
+ that require these common features.
7
+ """
8
+
9
+ def __getstate__(self):
10
+ state = self.__dict__.copy()
11
+ return state
12
+
13
+ def __setstate__(self, state):
14
+ self.__dict__.update(state)
@@ -0,0 +1,134 @@
1
+ import asyncio
2
+ import threading
3
+ from concurrent.futures import Future, ThreadPoolExecutor
4
+ from functools import partial
5
+ from typing import Any, Callable, Tuple
6
+
7
+ try:
8
+ import platform
9
+
10
+ import uvloop
11
+
12
+ if platform.python_implementation() != "PyPy":
13
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
14
+ except ImportError:
15
+ pass
16
+
17
+ from msgflux.envs import envs
18
+
19
+
20
+ class AsyncWorker:
21
+ def __init__(self):
22
+ """Initializes a worker with its own event loop in a separate thread."""
23
+ self.loop = asyncio.new_event_loop()
24
+ self.thread = threading.Thread(target=self.loop.run_forever, daemon=True)
25
+ self.thread.start()
26
+
27
+ def submit(self, coro):
28
+ """Submits a coroutine to the worker's event loop."""
29
+ return asyncio.run_coroutine_threadsafe(coro, self.loop)
30
+
31
+ def shutdown(self):
32
+ """Terminates the event loop and worker thread."""
33
+ self.loop.call_soon_threadsafe(self.loop.stop)
34
+ self.thread.join()
35
+
36
+
37
+ class Executor:
38
+ """Async pool to manage the execution of synchronous and asynchronous code.
39
+ Executor distributes tasks among a ThreadPool or among Async Workers.
40
+ """
41
+
42
+ _instance = None
43
+ _lock = threading.Lock()
44
+
45
+ def __init__(self):
46
+ self.num_threads = envs.executor_num_threads
47
+ self.num_async_workers = envs.executor_num_async_workers
48
+ self.async_worker_index = 0
49
+ self._thread_local = threading.local()
50
+ self.thread_pool = ThreadPoolExecutor(
51
+ max_workers=self.num_threads,
52
+ initializer=self._mark_thread_pool_worker,
53
+ )
54
+ self.async_workers = [AsyncWorker() for _ in range(self.num_async_workers)]
55
+
56
+ @classmethod
57
+ def get_instance(cls):
58
+ """Returns the singleton instance in a thread-safe manner."""
59
+ if cls._instance is None:
60
+ with cls._lock:
61
+ if cls._instance is None:
62
+ cls._instance = cls()
63
+ return cls._instance
64
+
65
+ def submit(self, f: Callable, *args, **kwargs):
66
+ """Submits a task to the appropriate pool based on the function type.
67
+ Returns a Future to track the result.
68
+ """
69
+ mode, target = self._resolve_submission_target(f, args, kwargs)
70
+ if mode == "async":
71
+ return self._submit_to_async_worker(target)
72
+
73
+ sync_callable, sync_args, sync_kwargs = target
74
+ if self._is_in_thread_pool_worker():
75
+ return self._execute_inline(sync_callable, *sync_args, **sync_kwargs)
76
+
77
+ return self.thread_pool.submit(sync_callable, *sync_args, **sync_kwargs)
78
+
79
+ def _submit_to_async_worker(self, coro):
80
+ """Distribute a coroutine to an asynchronous worker using round-robin."""
81
+ worker = self.async_workers[self.async_worker_index]
82
+ self.async_worker_index = (self.async_worker_index + 1) % self.num_async_workers
83
+ return worker.submit(coro)
84
+
85
+ def _resolve_submission_target(
86
+ self,
87
+ f: Callable,
88
+ args: Tuple[Any, ...],
89
+ kwargs: dict[str, Any],
90
+ ) -> Tuple[str, Any]:
91
+ """Resolve wrappers like functools.partial before deciding execution mode.
92
+
93
+ This preserves `.acall()` routing for partially-bound Modules/Tools and
94
+ avoids sending nested sync work back into the same saturated pool.
95
+ """
96
+ if isinstance(f, partial):
97
+ merged_kwargs = dict(f.keywords or {})
98
+ merged_kwargs.update(kwargs)
99
+ merged_args = (*f.args, *args)
100
+ return self._resolve_submission_target(f.func, merged_args, merged_kwargs)
101
+
102
+ if hasattr(f, "acall"):
103
+ return ("async", f.acall(*args, **kwargs))
104
+
105
+ if asyncio.iscoroutinefunction(f):
106
+ return ("async", f(*args, **kwargs))
107
+
108
+ return ("sync", (f, args, kwargs))
109
+
110
+ def _mark_thread_pool_worker(self) -> None:
111
+ self._thread_local.in_thread_pool_worker = True
112
+
113
+ def _is_in_thread_pool_worker(self) -> bool:
114
+ return bool(getattr(self._thread_local, "in_thread_pool_worker", False))
115
+
116
+ def _execute_inline(self, f: Callable, *args, **kwargs) -> Future:
117
+ """Execute nested sync work inline and wrap it in a completed Future."""
118
+ future: Future = Future()
119
+ try:
120
+ result = f(*args, **kwargs)
121
+ except Exception as exc:
122
+ future.set_exception(exc)
123
+ else:
124
+ future.set_result(result)
125
+ return future
126
+
127
+ def shutdown(self):
128
+ """Shutdown the executor, closing the pools."""
129
+ self.thread_pool.shutdown()
130
+ for worker in self.async_workers:
131
+ worker.shutdown()
132
+
133
+ def __del__(self):
134
+ self.shutdown()
@@ -0,0 +1,230 @@
1
+ import asyncio
2
+ import threading
3
+ from collections import deque
4
+ from typing import Any, AsyncGenerator, Literal, Optional, Union
5
+
6
+
7
+ class CoreResponse:
8
+ def set_metadata(self, metadata: Any):
9
+ self.metadata = metadata
10
+
11
+ def set_response_type(self, response_type: str):
12
+ if isinstance(response_type, str):
13
+ self.response_type = response_type
14
+ else:
15
+ raise TypeError(
16
+ f"`response_type` requires strgiven `{type(response_type)}`"
17
+ )
18
+
19
+
20
+ class BaseResponse(CoreResponse):
21
+ def __init__(self):
22
+ self.data = None
23
+ self.reasoning = None
24
+ self.metadata = None
25
+ self.response_type = None
26
+
27
+ @property
28
+ def has_reasoning(self) -> bool:
29
+ return self.reasoning is not None
30
+
31
+ def add(self, data: Any):
32
+ self.data = data
33
+
34
+ def consume(self) -> Any:
35
+ return self.data
36
+
37
+ def consume_reasoning(self) -> Optional[str]:
38
+ return self.reasoning
39
+
40
+
41
+ class BaseStreamResponse(CoreResponse):
42
+ def __init__(self, mode: Literal["sync", "async"] = "sync"):
43
+ if mode not in {"sync", "async"}:
44
+ raise ValueError("`mode` must be `sync` or `async`")
45
+ self.mode = mode
46
+ if mode == "async":
47
+ self.first_chunk_event = asyncio.Event()
48
+ self._response_type_event = asyncio.Event()
49
+ else:
50
+ self.first_chunk_event = threading.Event()
51
+ self._response_type_event = threading.Event()
52
+ self.data = None
53
+ self.reasoning = None
54
+ self.has_reasoning = False
55
+
56
+ # Content queue
57
+ self._queue = None
58
+ self._queue_loop = None
59
+ self._pending_chunks = deque()
60
+ self._queue_lock = threading.Lock()
61
+
62
+ # Reasoning queue
63
+ self._reasoning_queue = None
64
+ self._reasoning_queue_loop = None
65
+ self._reasoning_pending_chunks = deque()
66
+ self._reasoning_queue_lock = threading.Lock()
67
+
68
+ self.metadata = None
69
+ self.response_type = None
70
+ self.error = None
71
+
72
+ def _finish_queue_with_none(
73
+ self,
74
+ *,
75
+ queue_attr: str,
76
+ loop_attr: str,
77
+ pending_attr: str,
78
+ lock_attr: str,
79
+ ) -> None:
80
+ with getattr(self, lock_attr):
81
+ queue = getattr(self, queue_attr)
82
+ loop = getattr(self, loop_attr)
83
+ pending = getattr(self, pending_attr)
84
+ if queue is None or loop is None or loop.is_closed():
85
+ pending.append(None)
86
+ return
87
+
88
+ loop.call_soon_threadsafe(queue.put_nowait, None)
89
+
90
+ def _fail_stream(self, error: Exception) -> None:
91
+ self.set_error(error)
92
+ self._finish_queue_with_none(
93
+ queue_attr="_queue",
94
+ loop_attr="_queue_loop",
95
+ pending_attr="_pending_chunks",
96
+ lock_attr="_queue_lock",
97
+ )
98
+ self._finish_queue_with_none(
99
+ queue_attr="_reasoning_queue",
100
+ loop_attr="_reasoning_queue_loop",
101
+ pending_attr="_reasoning_pending_chunks",
102
+ lock_attr="_reasoning_queue_lock",
103
+ )
104
+
105
+ def _accumulate_data(self, data: Any) -> None:
106
+ if data is None:
107
+ return
108
+
109
+ if not isinstance(data, (str, bytes)):
110
+ raise TypeError(
111
+ "ModelStreamResponse only supports `str` or `bytes` chunks, "
112
+ f"got `{type(data).__name__}`."
113
+ )
114
+
115
+ if self.data is None:
116
+ self.data = data
117
+ return
118
+
119
+ if isinstance(self.data, str) and isinstance(data, str):
120
+ self.data += data
121
+ return
122
+
123
+ if isinstance(self.data, bytes) and isinstance(data, bytes):
124
+ self.data += data
125
+ return
126
+
127
+ raise TypeError(
128
+ "ModelStreamResponse received mixed chunk types: "
129
+ f"`{type(self.data).__name__}` then `{type(data).__name__}`."
130
+ )
131
+
132
+ def set_response_type(self, response_type: str):
133
+ super().set_response_type(response_type)
134
+ if not self._response_type_event.is_set():
135
+ self._response_type_event.set()
136
+
137
+ def set_error(self, error: Exception):
138
+ self.error = error
139
+ if not self.first_chunk_event.is_set():
140
+ self.first_chunk_event.set()
141
+ if not self._response_type_event.is_set():
142
+ self._response_type_event.set()
143
+
144
+ def add(self, data: Any):
145
+ """Add data to the content stream queue in a thread-safe way."""
146
+ if not self.first_chunk_event.is_set():
147
+ self.first_chunk_event.set()
148
+
149
+ try:
150
+ self._accumulate_data(data)
151
+ except Exception as e:
152
+ self._fail_stream(e)
153
+ raise
154
+
155
+ with self._queue_lock:
156
+ queue = self._queue
157
+ loop = self._queue_loop
158
+ if queue is None or loop is None or loop.is_closed():
159
+ self._pending_chunks.append(data)
160
+ return
161
+
162
+ loop.call_soon_threadsafe(queue.put_nowait, data)
163
+
164
+ def add_reasoning(self, data: Any):
165
+ """Add data to the reasoning stream queue in a thread-safe way."""
166
+ if data is not None and not self.has_reasoning:
167
+ self.has_reasoning = True
168
+ if not self.first_chunk_event.is_set():
169
+ self.first_chunk_event.set()
170
+ with self._reasoning_queue_lock:
171
+ queue = self._reasoning_queue
172
+ loop = self._reasoning_queue_loop
173
+ if queue is None or loop is None or loop.is_closed():
174
+ self._reasoning_pending_chunks.append(data)
175
+ return
176
+
177
+ loop.call_soon_threadsafe(queue.put_nowait, data)
178
+
179
+ def _bind_consumer_queue(self) -> asyncio.Queue:
180
+ loop = asyncio.get_running_loop()
181
+ with self._queue_lock:
182
+ if self._queue is None:
183
+ self._queue = asyncio.Queue()
184
+ self._queue_loop = loop
185
+ while self._pending_chunks:
186
+ self._queue.put_nowait(self._pending_chunks.popleft())
187
+ elif self._queue_loop is not loop:
188
+ raise RuntimeError(
189
+ "BaseStreamResponse.consume() must run on the same event loop."
190
+ )
191
+ return self._queue
192
+
193
+ def _bind_reasoning_queue(self) -> asyncio.Queue:
194
+ loop = asyncio.get_running_loop()
195
+ with self._reasoning_queue_lock:
196
+ if self._reasoning_queue is None:
197
+ self._reasoning_queue = asyncio.Queue()
198
+ self._reasoning_queue_loop = loop
199
+ while self._reasoning_pending_chunks:
200
+ self._reasoning_queue.put_nowait(
201
+ self._reasoning_pending_chunks.popleft()
202
+ )
203
+ elif self._reasoning_queue_loop is not loop:
204
+ raise RuntimeError(
205
+ "BaseStreamResponse.consume_reasoning() "
206
+ "must run on the same event loop."
207
+ )
208
+ return self._reasoning_queue
209
+
210
+ async def consume(self) -> AsyncGenerator[Union[bytes, str], None]:
211
+ """Async generator that yields content chunks until None is received."""
212
+ queue = self._bind_consumer_queue()
213
+ while True:
214
+ chunk = await queue.get()
215
+ if chunk is None:
216
+ if self.error is not None:
217
+ raise self.error
218
+ break
219
+ yield chunk
220
+
221
+ async def consume_reasoning(self) -> AsyncGenerator[str, None]:
222
+ """Async generator that yields reasoning chunks until None is received."""
223
+ queue = self._bind_reasoning_queue()
224
+ while True:
225
+ chunk = await queue.get()
226
+ if chunk is None:
227
+ if self.error is not None:
228
+ raise self.error
229
+ break
230
+ yield chunk
@@ -0,0 +1,5 @@
1
+ """Auto module for automatic parameter and configuration management."""
2
+
3
+ from msgflux.auto.params import AutoParams
4
+
5
+ __all__ = ["AutoParams"]