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/__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
|
msgflux/_private/core.py
ADDED
|
@@ -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
|