dataknobs-bots 0.2.4__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.
- dataknobs_bots/__init__.py +42 -0
- dataknobs_bots/api/__init__.py +42 -0
- dataknobs_bots/api/dependencies.py +140 -0
- dataknobs_bots/api/exceptions.py +289 -0
- dataknobs_bots/bot/__init__.py +15 -0
- dataknobs_bots/bot/base.py +1091 -0
- dataknobs_bots/bot/context.py +102 -0
- dataknobs_bots/bot/manager.py +430 -0
- dataknobs_bots/bot/registry.py +629 -0
- dataknobs_bots/config/__init__.py +39 -0
- dataknobs_bots/config/resolution.py +353 -0
- dataknobs_bots/knowledge/__init__.py +82 -0
- dataknobs_bots/knowledge/query/__init__.py +25 -0
- dataknobs_bots/knowledge/query/expander.py +262 -0
- dataknobs_bots/knowledge/query/transformer.py +288 -0
- dataknobs_bots/knowledge/rag.py +738 -0
- dataknobs_bots/knowledge/retrieval/__init__.py +23 -0
- dataknobs_bots/knowledge/retrieval/formatter.py +249 -0
- dataknobs_bots/knowledge/retrieval/merger.py +279 -0
- dataknobs_bots/memory/__init__.py +56 -0
- dataknobs_bots/memory/base.py +38 -0
- dataknobs_bots/memory/buffer.py +58 -0
- dataknobs_bots/memory/vector.py +188 -0
- dataknobs_bots/middleware/__init__.py +11 -0
- dataknobs_bots/middleware/base.py +92 -0
- dataknobs_bots/middleware/cost.py +421 -0
- dataknobs_bots/middleware/logging.py +184 -0
- dataknobs_bots/reasoning/__init__.py +65 -0
- dataknobs_bots/reasoning/base.py +50 -0
- dataknobs_bots/reasoning/react.py +299 -0
- dataknobs_bots/reasoning/simple.py +51 -0
- dataknobs_bots/registry/__init__.py +41 -0
- dataknobs_bots/registry/backend.py +181 -0
- dataknobs_bots/registry/memory.py +244 -0
- dataknobs_bots/registry/models.py +102 -0
- dataknobs_bots/registry/portability.py +210 -0
- dataknobs_bots/tools/__init__.py +5 -0
- dataknobs_bots/tools/knowledge_search.py +113 -0
- dataknobs_bots/utils/__init__.py +1 -0
- dataknobs_bots-0.2.4.dist-info/METADATA +591 -0
- dataknobs_bots-0.2.4.dist-info/RECORD +42 -0
- dataknobs_bots-0.2.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Bot execution context."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class BotContext:
|
|
9
|
+
"""Runtime context for bot execution.
|
|
10
|
+
|
|
11
|
+
Supports dict-like access for dynamic attributes via request_metadata.
|
|
12
|
+
Use `context["key"]` or `context.get("key")` for dynamic data.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
conversation_id: Unique identifier for the conversation
|
|
16
|
+
client_id: Identifier for the client/tenant
|
|
17
|
+
user_id: Optional user identifier
|
|
18
|
+
session_metadata: Metadata for the session
|
|
19
|
+
request_metadata: Metadata for the current request (also used for dict-like access)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
conversation_id: str
|
|
23
|
+
client_id: str
|
|
24
|
+
user_id: str | None = None
|
|
25
|
+
session_metadata: dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
request_metadata: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, key: str) -> Any:
|
|
29
|
+
"""Get item from request_metadata using dict-like access.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
key: Key to retrieve
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Value from request_metadata
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
KeyError: If key not found in request_metadata
|
|
39
|
+
"""
|
|
40
|
+
return self.request_metadata[key]
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
43
|
+
"""Set item in request_metadata using dict-like access.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
key: Key to set
|
|
47
|
+
value: Value to store
|
|
48
|
+
"""
|
|
49
|
+
self.request_metadata[key] = value
|
|
50
|
+
|
|
51
|
+
def __contains__(self, key: str) -> bool:
|
|
52
|
+
"""Check if key exists in request_metadata.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: Key to check
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if key exists in request_metadata
|
|
59
|
+
"""
|
|
60
|
+
return key in self.request_metadata
|
|
61
|
+
|
|
62
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
63
|
+
"""Get item from request_metadata with optional default.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
key: Key to retrieve
|
|
67
|
+
default: Default value if key not found
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Value from request_metadata or default
|
|
71
|
+
"""
|
|
72
|
+
return self.request_metadata.get(key, default)
|
|
73
|
+
|
|
74
|
+
def copy(self, **overrides: Any) -> "BotContext":
|
|
75
|
+
"""Create a copy of this context with optional field overrides.
|
|
76
|
+
|
|
77
|
+
Creates shallow copies of session_metadata and request_metadata dicts
|
|
78
|
+
to avoid mutation issues between the original and copy.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
**overrides: Field values to override in the copy
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
New BotContext instance with copied values
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> ctx = BotContext(conversation_id="conv-1", client_id="client-1")
|
|
88
|
+
>>> ctx2 = ctx.copy(conversation_id="conv-2")
|
|
89
|
+
>>> ctx2.conversation_id
|
|
90
|
+
'conv-2'
|
|
91
|
+
"""
|
|
92
|
+
return BotContext(
|
|
93
|
+
conversation_id=overrides.get("conversation_id", self.conversation_id),
|
|
94
|
+
client_id=overrides.get("client_id", self.client_id),
|
|
95
|
+
user_id=overrides.get("user_id", self.user_id),
|
|
96
|
+
session_metadata=overrides.get(
|
|
97
|
+
"session_metadata", dict(self.session_metadata)
|
|
98
|
+
),
|
|
99
|
+
request_metadata=overrides.get(
|
|
100
|
+
"request_metadata", dict(self.request_metadata)
|
|
101
|
+
),
|
|
102
|
+
)
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Bot manager for multi-tenant bot instances.
|
|
2
|
+
|
|
3
|
+
.. deprecated::
|
|
4
|
+
This module is deprecated. Use :class:`dataknobs_bots.bot.BotRegistry` instead,
|
|
5
|
+
which provides the same functionality plus persistent storage backends,
|
|
6
|
+
environment-aware configuration resolution, and TTL-based caching.
|
|
7
|
+
|
|
8
|
+
For simple in-memory usage, use :class:`dataknobs_bots.bot.InMemoryBotRegistry`.
|
|
9
|
+
|
|
10
|
+
Migration example::
|
|
11
|
+
|
|
12
|
+
# Old (deprecated)
|
|
13
|
+
from dataknobs_bots import BotManager
|
|
14
|
+
manager = BotManager()
|
|
15
|
+
bot = await manager.get_or_create("my-bot", config)
|
|
16
|
+
|
|
17
|
+
# New (recommended)
|
|
18
|
+
from dataknobs_bots.bot import InMemoryBotRegistry
|
|
19
|
+
registry = InMemoryBotRegistry(validate_on_register=False)
|
|
20
|
+
await registry.initialize()
|
|
21
|
+
await registry.register("my-bot", config)
|
|
22
|
+
bot = await registry.get_bot("my-bot")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import inspect
|
|
29
|
+
import logging
|
|
30
|
+
import warnings
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
|
33
|
+
|
|
34
|
+
from .base import DynaBot
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from dataknobs_config import EnvironmentAwareConfig, EnvironmentConfig
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_DEPRECATION_MESSAGE = (
|
|
42
|
+
"BotManager is deprecated and will be removed in a future version. "
|
|
43
|
+
"Use BotRegistry or InMemoryBotRegistry instead, which provide persistent "
|
|
44
|
+
"storage backends, environment-aware resolution, and TTL caching. "
|
|
45
|
+
"See dataknobs_bots.bot.BotRegistry for details."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@runtime_checkable
|
|
50
|
+
class ConfigLoader(Protocol):
|
|
51
|
+
"""Protocol for configuration loaders with a load method."""
|
|
52
|
+
|
|
53
|
+
def load(self, bot_id: str) -> dict[str, Any]:
|
|
54
|
+
"""Load configuration for a bot."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class AsyncConfigLoader(Protocol):
|
|
60
|
+
"""Protocol for async configuration loaders."""
|
|
61
|
+
|
|
62
|
+
async def load(self, bot_id: str) -> dict[str, Any]:
|
|
63
|
+
"""Load configuration for a bot asynchronously."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ConfigLoaderType = (
|
|
68
|
+
ConfigLoader
|
|
69
|
+
| AsyncConfigLoader
|
|
70
|
+
| Callable[[str], dict[str, Any]]
|
|
71
|
+
| Callable[[str], Any] # For async callables
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BotManager:
|
|
76
|
+
"""Manages multiple DynaBot instances for multi-tenancy.
|
|
77
|
+
|
|
78
|
+
.. deprecated::
|
|
79
|
+
Use :class:`BotRegistry` or :class:`InMemoryBotRegistry` instead.
|
|
80
|
+
|
|
81
|
+
BotManager handles:
|
|
82
|
+
- Bot instance creation and caching
|
|
83
|
+
- Client-level isolation
|
|
84
|
+
- Configuration loading and validation
|
|
85
|
+
- Bot lifecycle management
|
|
86
|
+
- Environment-aware resource resolution (optional)
|
|
87
|
+
|
|
88
|
+
Each client/tenant gets its own bot instance, which can serve multiple users.
|
|
89
|
+
The underlying DynaBot architecture ensures conversation isolation through
|
|
90
|
+
BotContext with different conversation_ids.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
bots: Cache of bot_id -> DynaBot instances
|
|
94
|
+
config_loader: Optional configuration loader (sync or async)
|
|
95
|
+
environment_name: Current environment name (if environment-aware)
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
```python
|
|
99
|
+
# Basic usage with inline configuration
|
|
100
|
+
manager = BotManager()
|
|
101
|
+
bot = await manager.get_or_create("my-bot", config={
|
|
102
|
+
"llm": {"provider": "openai", "model": "gpt-4o"},
|
|
103
|
+
"conversation_storage": {"backend": "memory"},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# With environment-aware configuration
|
|
107
|
+
manager = BotManager(environment="production")
|
|
108
|
+
bot = await manager.get_or_create("my-bot", config={
|
|
109
|
+
"bot": {
|
|
110
|
+
"llm": {"$resource": "default", "type": "llm_providers"},
|
|
111
|
+
"conversation_storage": {"$resource": "db", "type": "databases"},
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# With config loader function
|
|
116
|
+
def load_config(bot_id: str) -> dict:
|
|
117
|
+
return load_yaml(f"configs/{bot_id}.yaml")
|
|
118
|
+
|
|
119
|
+
manager = BotManager(config_loader=load_config)
|
|
120
|
+
bot = await manager.get_or_create("my-bot")
|
|
121
|
+
|
|
122
|
+
# List active bots
|
|
123
|
+
active_bots = manager.list_bots()
|
|
124
|
+
```
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
config_loader: ConfigLoaderType | None = None,
|
|
130
|
+
environment: EnvironmentConfig | str | None = None,
|
|
131
|
+
env_dir: str | Path = "config/environments",
|
|
132
|
+
):
|
|
133
|
+
"""Initialize BotManager.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config_loader: Optional configuration loader.
|
|
137
|
+
Can be:
|
|
138
|
+
- An object with a `.load(bot_id)` method (sync or async)
|
|
139
|
+
- A callable function: bot_id -> config_dict (sync or async)
|
|
140
|
+
- None (configurations must be provided explicitly)
|
|
141
|
+
environment: Environment name or EnvironmentConfig for resource resolution.
|
|
142
|
+
If None, environment-aware features are disabled unless
|
|
143
|
+
an EnvironmentAwareConfig is passed to get_or_create().
|
|
144
|
+
If a string, loads environment config from env_dir.
|
|
145
|
+
env_dir: Directory containing environment config files.
|
|
146
|
+
Only used if environment is a string name.
|
|
147
|
+
"""
|
|
148
|
+
warnings.warn(_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2)
|
|
149
|
+
|
|
150
|
+
self._bots: dict[str, DynaBot] = {}
|
|
151
|
+
self._config_loader = config_loader
|
|
152
|
+
self._env_dir = Path(env_dir)
|
|
153
|
+
|
|
154
|
+
# Load environment config if specified
|
|
155
|
+
self._environment: EnvironmentConfig | None = None
|
|
156
|
+
if environment is not None:
|
|
157
|
+
try:
|
|
158
|
+
from dataknobs_config import EnvironmentConfig
|
|
159
|
+
|
|
160
|
+
if isinstance(environment, str):
|
|
161
|
+
self._environment = EnvironmentConfig.load(environment, env_dir)
|
|
162
|
+
else:
|
|
163
|
+
self._environment = environment
|
|
164
|
+
logger.info(f"Initialized BotManager with environment: {self._environment.name}")
|
|
165
|
+
except ImportError:
|
|
166
|
+
logger.warning(
|
|
167
|
+
"dataknobs_config not installed, environment-aware features disabled"
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
logger.info("Initialized BotManager")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def environment_name(self) -> str | None:
|
|
174
|
+
"""Get current environment name, or None if not environment-aware."""
|
|
175
|
+
return self._environment.name if self._environment else None
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def environment(self) -> EnvironmentConfig | None:
|
|
179
|
+
"""Get current environment config, or None if not environment-aware."""
|
|
180
|
+
return self._environment
|
|
181
|
+
|
|
182
|
+
async def get_or_create(
|
|
183
|
+
self,
|
|
184
|
+
bot_id: str,
|
|
185
|
+
config: dict[str, Any] | EnvironmentAwareConfig | None = None,
|
|
186
|
+
use_environment: bool | None = None,
|
|
187
|
+
config_key: str = "bot",
|
|
188
|
+
) -> DynaBot:
|
|
189
|
+
"""Get existing bot or create new one.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
bot_id: Bot identifier (e.g., "customer-support", "sales-assistant")
|
|
193
|
+
config: Optional bot configuration. Can be:
|
|
194
|
+
- dict with resolved values (traditional)
|
|
195
|
+
- dict with $resource references (requires environment)
|
|
196
|
+
- EnvironmentAwareConfig instance
|
|
197
|
+
If not provided and config_loader is set, will load configuration.
|
|
198
|
+
use_environment: Whether to use environment-aware resolution.
|
|
199
|
+
- True: Use environment for $resource resolution
|
|
200
|
+
- False: Use config as-is (no resolution)
|
|
201
|
+
- None (default): Auto-detect based on whether manager has
|
|
202
|
+
an environment configured or config is EnvironmentAwareConfig
|
|
203
|
+
config_key: Key within config containing bot configuration.
|
|
204
|
+
Defaults to "bot". Set to None to use root config.
|
|
205
|
+
Only used when use_environment is True.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
DynaBot instance
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ValueError: If config is None and no config_loader is set
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
```python
|
|
215
|
+
# Traditional usage (no environment resolution)
|
|
216
|
+
manager = BotManager()
|
|
217
|
+
bot = await manager.get_or_create("support-bot", config={
|
|
218
|
+
"llm": {"provider": "openai", "model": "gpt-4"},
|
|
219
|
+
"conversation_storage": {"backend": "memory"},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
# Environment-aware usage with $resource references
|
|
223
|
+
manager = BotManager(environment="production")
|
|
224
|
+
bot = await manager.get_or_create("support-bot", config={
|
|
225
|
+
"bot": {
|
|
226
|
+
"llm": {"$resource": "default", "type": "llm_providers"},
|
|
227
|
+
"conversation_storage": {"$resource": "db", "type": "databases"},
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
# Explicit environment resolution control
|
|
232
|
+
bot = await manager.get_or_create(
|
|
233
|
+
"support-bot",
|
|
234
|
+
config=my_config,
|
|
235
|
+
use_environment=True,
|
|
236
|
+
config_key="bot"
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
"""
|
|
240
|
+
# Return cached bot if exists
|
|
241
|
+
if bot_id in self._bots:
|
|
242
|
+
logger.debug(f"Returning cached bot: {bot_id}")
|
|
243
|
+
return self._bots[bot_id]
|
|
244
|
+
|
|
245
|
+
# Load configuration if not provided
|
|
246
|
+
if config is None:
|
|
247
|
+
if self._config_loader is None:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"No configuration provided for bot '{bot_id}' "
|
|
250
|
+
"and no config_loader is set"
|
|
251
|
+
)
|
|
252
|
+
config = await self._load_config(bot_id)
|
|
253
|
+
|
|
254
|
+
# Determine whether to use environment resolution
|
|
255
|
+
is_env_aware_config = False
|
|
256
|
+
try:
|
|
257
|
+
from dataknobs_config import EnvironmentAwareConfig
|
|
258
|
+
|
|
259
|
+
is_env_aware_config = isinstance(config, EnvironmentAwareConfig)
|
|
260
|
+
except ImportError:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
should_use_environment = use_environment
|
|
264
|
+
if should_use_environment is None:
|
|
265
|
+
# Auto-detect: use environment if manager has one or config is EnvironmentAwareConfig
|
|
266
|
+
should_use_environment = self._environment is not None or is_env_aware_config
|
|
267
|
+
|
|
268
|
+
# Create new bot
|
|
269
|
+
logger.info(f"Creating new bot: {bot_id} (environment_aware={should_use_environment})")
|
|
270
|
+
|
|
271
|
+
if should_use_environment:
|
|
272
|
+
bot = await DynaBot.from_environment_aware_config(
|
|
273
|
+
config,
|
|
274
|
+
environment=self._environment,
|
|
275
|
+
env_dir=self._env_dir,
|
|
276
|
+
config_key=config_key,
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
# Traditional path - use config as-is
|
|
280
|
+
bot = await DynaBot.from_config(config)
|
|
281
|
+
|
|
282
|
+
# Cache and return
|
|
283
|
+
self._bots[bot_id] = bot
|
|
284
|
+
return bot
|
|
285
|
+
|
|
286
|
+
async def get(self, bot_id: str) -> DynaBot | None:
|
|
287
|
+
"""Get bot without creating if doesn't exist.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
bot_id: Bot identifier
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
DynaBot instance if exists, None otherwise
|
|
294
|
+
"""
|
|
295
|
+
return self._bots.get(bot_id)
|
|
296
|
+
|
|
297
|
+
async def remove(self, bot_id: str) -> bool:
|
|
298
|
+
"""Remove bot instance.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
bot_id: Bot identifier
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
True if bot was removed, False if didn't exist
|
|
305
|
+
"""
|
|
306
|
+
if bot_id in self._bots:
|
|
307
|
+
logger.info(f"Removing bot: {bot_id}")
|
|
308
|
+
del self._bots[bot_id]
|
|
309
|
+
return True
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
async def reload(self, bot_id: str) -> DynaBot:
|
|
313
|
+
"""Reload bot instance with fresh configuration.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
bot_id: Bot identifier
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
New DynaBot instance
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
ValueError: If no config_loader is set
|
|
323
|
+
"""
|
|
324
|
+
if self._config_loader is None:
|
|
325
|
+
raise ValueError("Cannot reload without config_loader")
|
|
326
|
+
|
|
327
|
+
# Remove existing bot
|
|
328
|
+
await self.remove(bot_id)
|
|
329
|
+
|
|
330
|
+
# Create new one
|
|
331
|
+
return await self.get_or_create(bot_id)
|
|
332
|
+
|
|
333
|
+
def list_bots(self) -> list[str]:
|
|
334
|
+
"""List all active bot IDs.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of bot identifiers
|
|
338
|
+
"""
|
|
339
|
+
return list(self._bots.keys())
|
|
340
|
+
|
|
341
|
+
def get_bot_count(self) -> int:
|
|
342
|
+
"""Get count of active bots.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Number of active bot instances
|
|
346
|
+
"""
|
|
347
|
+
return len(self._bots)
|
|
348
|
+
|
|
349
|
+
async def _load_config(self, bot_id: str) -> dict[str, Any]:
|
|
350
|
+
"""Load configuration for bot using config_loader.
|
|
351
|
+
|
|
352
|
+
Supports both synchronous and asynchronous config loaders.
|
|
353
|
+
Handles both callable loaders and objects with a load() method.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
bot_id: Bot identifier
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Bot configuration dictionary
|
|
360
|
+
"""
|
|
361
|
+
logger.debug(f"Loading configuration for bot: {bot_id}")
|
|
362
|
+
|
|
363
|
+
if callable(self._config_loader):
|
|
364
|
+
# Handle callable config loader (function)
|
|
365
|
+
if inspect.iscoroutinefunction(self._config_loader):
|
|
366
|
+
# Async function
|
|
367
|
+
result = await self._config_loader(bot_id)
|
|
368
|
+
return dict(result) if isinstance(result, dict) else {}
|
|
369
|
+
else:
|
|
370
|
+
# Sync function - run in executor to avoid blocking
|
|
371
|
+
loop = asyncio.get_event_loop()
|
|
372
|
+
result = await loop.run_in_executor(None, self._config_loader, bot_id)
|
|
373
|
+
return dict(result) if isinstance(result, dict) else {}
|
|
374
|
+
else:
|
|
375
|
+
# Assume it's an object with a load method
|
|
376
|
+
load_method = self._config_loader.load # type: ignore
|
|
377
|
+
|
|
378
|
+
if inspect.iscoroutinefunction(load_method):
|
|
379
|
+
# Async method
|
|
380
|
+
result = await load_method(bot_id)
|
|
381
|
+
return dict(result) if isinstance(result, dict) else {}
|
|
382
|
+
else:
|
|
383
|
+
# Sync method - run in executor to avoid blocking
|
|
384
|
+
loop = asyncio.get_event_loop()
|
|
385
|
+
result = await loop.run_in_executor(None, load_method, bot_id)
|
|
386
|
+
return dict(result) if isinstance(result, dict) else {}
|
|
387
|
+
|
|
388
|
+
async def clear_all(self) -> None:
|
|
389
|
+
"""Clear all bot instances.
|
|
390
|
+
|
|
391
|
+
Useful for testing or when restarting the service.
|
|
392
|
+
"""
|
|
393
|
+
logger.info("Clearing all bot instances")
|
|
394
|
+
self._bots.clear()
|
|
395
|
+
|
|
396
|
+
def get_portable_config(
|
|
397
|
+
self,
|
|
398
|
+
config: dict[str, Any] | EnvironmentAwareConfig,
|
|
399
|
+
) -> dict[str, Any]:
|
|
400
|
+
"""Get portable configuration for storage.
|
|
401
|
+
|
|
402
|
+
Extracts portable config (with $resource references intact,
|
|
403
|
+
environment variables unresolved) suitable for storing in
|
|
404
|
+
registries or databases.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
config: Configuration to make portable.
|
|
408
|
+
Can be dict or EnvironmentAwareConfig.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Portable configuration dictionary
|
|
412
|
+
|
|
413
|
+
Example:
|
|
414
|
+
```python
|
|
415
|
+
manager = BotManager(environment="production")
|
|
416
|
+
|
|
417
|
+
# Get portable config from EnvironmentAwareConfig
|
|
418
|
+
portable = manager.get_portable_config(env_aware_config)
|
|
419
|
+
|
|
420
|
+
# Store in registry (portable across environments)
|
|
421
|
+
await registry.store(bot_id, portable)
|
|
422
|
+
```
|
|
423
|
+
"""
|
|
424
|
+
return DynaBot.get_portable_config(config)
|
|
425
|
+
|
|
426
|
+
def __repr__(self) -> str:
|
|
427
|
+
"""String representation."""
|
|
428
|
+
bots = ", ".join(self._bots.keys())
|
|
429
|
+
env = f", environment={self._environment.name!r}" if self._environment else ""
|
|
430
|
+
return f"BotManager(bots=[{bots}], count={len(self._bots)}{env})"
|