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,50 @@
|
|
|
1
|
+
"""Base reasoning strategy for DynaBot."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReasoningStrategy(ABC):
|
|
8
|
+
"""Abstract base class for reasoning strategies.
|
|
9
|
+
|
|
10
|
+
Reasoning strategies control how the bot processes information
|
|
11
|
+
and generates responses. Different strategies can implement
|
|
12
|
+
different levels of reasoning complexity.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
- Simple: Direct LLM call
|
|
16
|
+
- Chain-of-Thought: Break down reasoning into steps
|
|
17
|
+
- ReAct: Reason and act in a loop with tools
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def generate(
|
|
22
|
+
self,
|
|
23
|
+
manager: Any,
|
|
24
|
+
llm: Any,
|
|
25
|
+
tools: list[Any] | None = None,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
) -> Any:
|
|
28
|
+
"""Generate response using this reasoning strategy.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
manager: ConversationManager instance
|
|
32
|
+
llm: LLM provider instance
|
|
33
|
+
tools: Optional list of available tools
|
|
34
|
+
**kwargs: Additional generation parameters (temperature, max_tokens, etc.)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
LLM response object
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
```python
|
|
41
|
+
response = await strategy.generate(
|
|
42
|
+
manager=conversation_manager,
|
|
43
|
+
llm=llm_provider,
|
|
44
|
+
tools=[search_tool, calculator_tool],
|
|
45
|
+
temperature=0.7,
|
|
46
|
+
max_tokens=1000
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""ReAct (Reasoning + Acting) reasoning strategy."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import ReasoningStrategy
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReActReasoning(ReasoningStrategy):
|
|
12
|
+
"""ReAct (Reasoning + Acting) strategy.
|
|
13
|
+
|
|
14
|
+
This strategy implements the ReAct pattern where the LLM:
|
|
15
|
+
1. Reasons about what to do (Thought)
|
|
16
|
+
2. Takes an action (using tools if needed)
|
|
17
|
+
3. Observes the result
|
|
18
|
+
4. Repeats until task is complete
|
|
19
|
+
|
|
20
|
+
This is useful for:
|
|
21
|
+
- Multi-step problem solving
|
|
22
|
+
- Tasks requiring tool use
|
|
23
|
+
- Complex reasoning chains
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
max_iterations: Maximum number of reasoning loops
|
|
27
|
+
verbose: Whether to enable debug-level logging
|
|
28
|
+
store_trace: Whether to store reasoning trace in conversation metadata
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
```python
|
|
32
|
+
strategy = ReActReasoning(
|
|
33
|
+
max_iterations=5,
|
|
34
|
+
verbose=True,
|
|
35
|
+
store_trace=True
|
|
36
|
+
)
|
|
37
|
+
response = await strategy.generate(
|
|
38
|
+
manager=conversation_manager,
|
|
39
|
+
llm=llm_provider,
|
|
40
|
+
tools=[search_tool, calculator_tool]
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
max_iterations: int = 5,
|
|
48
|
+
verbose: bool = False,
|
|
49
|
+
store_trace: bool = False,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize ReAct reasoning strategy.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
max_iterations: Maximum reasoning/action iterations
|
|
55
|
+
verbose: Enable debug-level logging for reasoning steps
|
|
56
|
+
store_trace: Store reasoning trace in conversation metadata
|
|
57
|
+
"""
|
|
58
|
+
self.max_iterations = max_iterations
|
|
59
|
+
self.verbose = verbose
|
|
60
|
+
self.store_trace = store_trace
|
|
61
|
+
|
|
62
|
+
async def generate(
|
|
63
|
+
self,
|
|
64
|
+
manager: Any,
|
|
65
|
+
llm: Any,
|
|
66
|
+
tools: list[Any] | None = None,
|
|
67
|
+
**kwargs: Any,
|
|
68
|
+
) -> Any:
|
|
69
|
+
"""Generate response using ReAct loop.
|
|
70
|
+
|
|
71
|
+
The ReAct loop:
|
|
72
|
+
1. Generate response (may include tool calls)
|
|
73
|
+
2. If tool calls present, execute them
|
|
74
|
+
3. Add observations to conversation
|
|
75
|
+
4. Repeat until no more tool calls or max iterations
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
manager: ConversationManager instance
|
|
79
|
+
llm: LLM provider instance
|
|
80
|
+
tools: Optional list of available tools
|
|
81
|
+
**kwargs: Generation parameters
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Final LLM response
|
|
85
|
+
"""
|
|
86
|
+
if not tools:
|
|
87
|
+
# No tools available, fall back to simple generation
|
|
88
|
+
logger.info(
|
|
89
|
+
"ReAct: No tools available, falling back to simple generation",
|
|
90
|
+
extra={"conversation_id": manager.conversation_id},
|
|
91
|
+
)
|
|
92
|
+
return await manager.complete(**kwargs)
|
|
93
|
+
|
|
94
|
+
# Initialize trace if enabled
|
|
95
|
+
trace = [] if self.store_trace else None
|
|
96
|
+
|
|
97
|
+
# Get log level based on verbose setting
|
|
98
|
+
log_level = logging.DEBUG if self.verbose else logging.INFO
|
|
99
|
+
|
|
100
|
+
logger.log(
|
|
101
|
+
log_level,
|
|
102
|
+
"ReAct: Starting reasoning loop",
|
|
103
|
+
extra={
|
|
104
|
+
"conversation_id": manager.conversation_id,
|
|
105
|
+
"max_iterations": self.max_iterations,
|
|
106
|
+
"tools_available": len(tools),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# ReAct loop
|
|
111
|
+
for iteration in range(self.max_iterations):
|
|
112
|
+
iteration_trace = {
|
|
113
|
+
"iteration": iteration + 1,
|
|
114
|
+
"tool_calls": [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.log(
|
|
118
|
+
log_level,
|
|
119
|
+
"ReAct: Starting iteration",
|
|
120
|
+
extra={
|
|
121
|
+
"conversation_id": manager.conversation_id,
|
|
122
|
+
"iteration": iteration + 1,
|
|
123
|
+
"max_iterations": self.max_iterations,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Generate response with tools
|
|
128
|
+
response = await manager.complete(tools=tools, **kwargs)
|
|
129
|
+
|
|
130
|
+
# Check if we have tool calls
|
|
131
|
+
if not hasattr(response, "tool_calls") or not response.tool_calls:
|
|
132
|
+
# No tool calls, we're done
|
|
133
|
+
logger.log(
|
|
134
|
+
log_level,
|
|
135
|
+
"ReAct: No tool calls in response, finishing",
|
|
136
|
+
extra={
|
|
137
|
+
"conversation_id": manager.conversation_id,
|
|
138
|
+
"iteration": iteration + 1,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if trace is not None:
|
|
143
|
+
iteration_trace["status"] = "completed"
|
|
144
|
+
trace.append(iteration_trace)
|
|
145
|
+
await self._store_trace(manager, trace)
|
|
146
|
+
|
|
147
|
+
return response
|
|
148
|
+
|
|
149
|
+
num_tool_calls = len(response.tool_calls)
|
|
150
|
+
logger.log(
|
|
151
|
+
log_level,
|
|
152
|
+
"ReAct: Executing tool calls",
|
|
153
|
+
extra={
|
|
154
|
+
"conversation_id": manager.conversation_id,
|
|
155
|
+
"iteration": iteration + 1,
|
|
156
|
+
"num_tools": num_tool_calls,
|
|
157
|
+
"tools": [tc.name for tc in response.tool_calls],
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Execute all tool calls
|
|
162
|
+
for tool_call in response.tool_calls:
|
|
163
|
+
tool_trace = {
|
|
164
|
+
"name": tool_call.name,
|
|
165
|
+
"parameters": tool_call.parameters,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
# Find the tool
|
|
170
|
+
tool = self._find_tool(tool_call.name, tools)
|
|
171
|
+
if not tool:
|
|
172
|
+
observation = f"Error: Tool '{tool_call.name}' not found"
|
|
173
|
+
tool_trace["status"] = "error"
|
|
174
|
+
tool_trace["error"] = "Tool not found"
|
|
175
|
+
|
|
176
|
+
logger.warning(
|
|
177
|
+
"ReAct: Tool not found",
|
|
178
|
+
extra={
|
|
179
|
+
"conversation_id": manager.conversation_id,
|
|
180
|
+
"iteration": iteration + 1,
|
|
181
|
+
"tool_name": tool_call.name,
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
# Execute the tool
|
|
186
|
+
result = await tool.execute(**tool_call.parameters)
|
|
187
|
+
observation = f"Tool result: {result}"
|
|
188
|
+
tool_trace["status"] = "success"
|
|
189
|
+
tool_trace["result"] = str(result)
|
|
190
|
+
|
|
191
|
+
logger.log(
|
|
192
|
+
log_level,
|
|
193
|
+
"ReAct: Tool executed successfully",
|
|
194
|
+
extra={
|
|
195
|
+
"conversation_id": manager.conversation_id,
|
|
196
|
+
"iteration": iteration + 1,
|
|
197
|
+
"tool_name": tool_call.name,
|
|
198
|
+
"result_length": len(str(result)),
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Add observation to conversation
|
|
203
|
+
await manager.add_message(
|
|
204
|
+
content=f"Observation from {tool_call.name}: {observation}",
|
|
205
|
+
role="system",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
# Handle tool execution errors
|
|
210
|
+
error_msg = f"Error executing tool {tool_call.name}: {e!s}"
|
|
211
|
+
tool_trace["status"] = "error"
|
|
212
|
+
tool_trace["error"] = str(e)
|
|
213
|
+
|
|
214
|
+
logger.error(
|
|
215
|
+
"ReAct: Tool execution failed",
|
|
216
|
+
extra={
|
|
217
|
+
"conversation_id": manager.conversation_id,
|
|
218
|
+
"iteration": iteration + 1,
|
|
219
|
+
"tool_name": tool_call.name,
|
|
220
|
+
"error": str(e),
|
|
221
|
+
},
|
|
222
|
+
exc_info=True,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
await manager.add_message(content=error_msg, role="system")
|
|
226
|
+
|
|
227
|
+
if trace is not None:
|
|
228
|
+
iteration_trace["tool_calls"].append(tool_trace)
|
|
229
|
+
|
|
230
|
+
if trace is not None:
|
|
231
|
+
iteration_trace["status"] = "continued"
|
|
232
|
+
trace.append(iteration_trace)
|
|
233
|
+
|
|
234
|
+
# Max iterations reached, generate final response without tools
|
|
235
|
+
logger.log(
|
|
236
|
+
log_level,
|
|
237
|
+
"ReAct: Max iterations reached, generating final response",
|
|
238
|
+
extra={
|
|
239
|
+
"conversation_id": manager.conversation_id,
|
|
240
|
+
"iterations_used": self.max_iterations,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if trace is not None:
|
|
245
|
+
trace.append({"status": "max_iterations_reached"})
|
|
246
|
+
await self._store_trace(manager, trace)
|
|
247
|
+
|
|
248
|
+
return await manager.complete(**kwargs)
|
|
249
|
+
|
|
250
|
+
async def _store_trace(self, manager: Any, trace: list[dict[str, Any]]) -> None:
|
|
251
|
+
"""Store reasoning trace in conversation metadata.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
manager: ConversationManager instance
|
|
255
|
+
trace: Reasoning trace data
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
# Get existing metadata
|
|
259
|
+
metadata = manager.conversation.metadata or {}
|
|
260
|
+
|
|
261
|
+
# Add trace to metadata
|
|
262
|
+
metadata["reasoning_trace"] = trace
|
|
263
|
+
|
|
264
|
+
# Update conversation metadata
|
|
265
|
+
await manager.storage.update_metadata(
|
|
266
|
+
conversation_id=manager.conversation_id,
|
|
267
|
+
metadata=metadata,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
logger.debug(
|
|
271
|
+
"ReAct: Stored reasoning trace in conversation metadata",
|
|
272
|
+
extra={
|
|
273
|
+
"conversation_id": manager.conversation_id,
|
|
274
|
+
"trace_items": len(trace),
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.warning(
|
|
279
|
+
"ReAct: Failed to store reasoning trace",
|
|
280
|
+
extra={
|
|
281
|
+
"conversation_id": manager.conversation_id,
|
|
282
|
+
"error": str(e),
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _find_tool(self, tool_name: str, tools: list[Any]) -> Any | None:
|
|
287
|
+
"""Find a tool by name.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
tool_name: Name of the tool to find
|
|
291
|
+
tools: List of available tools
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Tool instance or None if not found
|
|
295
|
+
"""
|
|
296
|
+
for tool in tools:
|
|
297
|
+
if tool.name == tool_name:
|
|
298
|
+
return tool
|
|
299
|
+
return None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Simple reasoning strategy - direct LLM call."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import ReasoningStrategy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleReasoning(ReasoningStrategy):
|
|
9
|
+
"""Simple reasoning strategy that makes direct LLM calls.
|
|
10
|
+
|
|
11
|
+
This is the most straightforward strategy - it simply passes
|
|
12
|
+
the conversation to the LLM and returns the response without
|
|
13
|
+
any additional reasoning steps.
|
|
14
|
+
|
|
15
|
+
Use this when:
|
|
16
|
+
- You want direct, fast responses
|
|
17
|
+
- The task doesn't require complex reasoning
|
|
18
|
+
- You're using a powerful model that doesn't need guidance
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
```python
|
|
22
|
+
strategy = SimpleReasoning()
|
|
23
|
+
response = await strategy.generate(
|
|
24
|
+
manager=conversation_manager,
|
|
25
|
+
llm=llm_provider,
|
|
26
|
+
temperature=0.7
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def generate(
|
|
32
|
+
self,
|
|
33
|
+
manager: Any,
|
|
34
|
+
llm: Any,
|
|
35
|
+
tools: list[Any] | None = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Generate response with a simple LLM call.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
manager: ConversationManager instance
|
|
42
|
+
llm: LLM provider instance (not used directly)
|
|
43
|
+
tools: Optional list of tools
|
|
44
|
+
**kwargs: Generation parameters
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
LLM response
|
|
48
|
+
"""
|
|
49
|
+
# Use the conversation manager's generate method
|
|
50
|
+
# which handles the LLM call with the conversation history
|
|
51
|
+
return await manager.complete(tools=tools, **kwargs)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Registry module for bot registration storage and management.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Registration: Dataclass for bot registration with metadata
|
|
5
|
+
- RegistryBackend: Protocol for pluggable storage backends
|
|
6
|
+
- InMemoryBackend: Simple dict-based storage for testing/development
|
|
7
|
+
- Portability validation utilities
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
```python
|
|
11
|
+
from dataknobs_bots.registry import Registration, InMemoryBackend
|
|
12
|
+
|
|
13
|
+
backend = InMemoryBackend()
|
|
14
|
+
await backend.initialize()
|
|
15
|
+
|
|
16
|
+
reg = await backend.register("my-bot", {"llm": {...}})
|
|
17
|
+
print(f"Bot registered at {reg.created_at}")
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .backend import RegistryBackend
|
|
24
|
+
from .memory import InMemoryBackend
|
|
25
|
+
from .models import Registration
|
|
26
|
+
from .portability import (
|
|
27
|
+
PortabilityError,
|
|
28
|
+
has_resource_references,
|
|
29
|
+
is_portable,
|
|
30
|
+
validate_portability,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Registration",
|
|
35
|
+
"RegistryBackend",
|
|
36
|
+
"InMemoryBackend",
|
|
37
|
+
"PortabilityError",
|
|
38
|
+
"validate_portability",
|
|
39
|
+
"has_resource_references",
|
|
40
|
+
"is_portable",
|
|
41
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Registry backend protocol for pluggable storage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .models import Registration
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class RegistryBackend(Protocol):
|
|
13
|
+
"""Protocol for bot registry storage backends.
|
|
14
|
+
|
|
15
|
+
Implementations store bot configurations with metadata.
|
|
16
|
+
The backend is responsible for persistence; the BotRegistry
|
|
17
|
+
handles caching and bot instantiation.
|
|
18
|
+
|
|
19
|
+
This protocol defines the interface for storage backends:
|
|
20
|
+
- InMemoryBackend: Simple dict storage (default, good for tests)
|
|
21
|
+
- PostgreSQLBackend: Database persistence (future/external)
|
|
22
|
+
- RedisBackend: Distributed caching (future/external)
|
|
23
|
+
|
|
24
|
+
All methods are async to support both sync and async backends.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
class MyCustomBackend:
|
|
29
|
+
async def initialize(self) -> None:
|
|
30
|
+
# Setup database connection
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
async def register(self, bot_id: str, config: dict, status: str = "active"):
|
|
34
|
+
# Store in database
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
# ... implement other methods
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
async def initialize(self) -> None:
|
|
42
|
+
"""Initialize the backend (create tables, connections, etc.).
|
|
43
|
+
|
|
44
|
+
Called before the backend is used. Should be idempotent.
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
async def close(self) -> None:
|
|
49
|
+
"""Close the backend (release connections, etc.).
|
|
50
|
+
|
|
51
|
+
Called when the registry is shutting down.
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def register(
|
|
56
|
+
self,
|
|
57
|
+
bot_id: str,
|
|
58
|
+
config: dict[str, Any],
|
|
59
|
+
status: str = "active",
|
|
60
|
+
) -> Registration:
|
|
61
|
+
"""Register a bot or update existing registration.
|
|
62
|
+
|
|
63
|
+
If a registration with the same bot_id exists, it should be updated
|
|
64
|
+
(config replaced, status updated, updated_at set to now).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
bot_id: Unique bot identifier
|
|
68
|
+
config: Bot configuration dictionary (should be portable)
|
|
69
|
+
status: Registration status (default: active)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Registration object with metadata
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
async def get(self, bot_id: str) -> Registration | None:
|
|
77
|
+
"""Get registration by ID.
|
|
78
|
+
|
|
79
|
+
Should update last_accessed_at timestamp on access.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
bot_id: Bot identifier
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Registration if found, None otherwise
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def get_config(self, bot_id: str) -> dict[str, Any] | None:
|
|
90
|
+
"""Get just the config for a bot.
|
|
91
|
+
|
|
92
|
+
Convenience method that returns only the config dict.
|
|
93
|
+
Should also update last_accessed_at.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
bot_id: Bot identifier
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Config dict if found, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
async def exists(self, bot_id: str) -> bool:
|
|
104
|
+
"""Check if an active registration exists.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
bot_id: Bot identifier
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if registration exists and is active
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
async def unregister(self, bot_id: str) -> bool:
|
|
115
|
+
"""Hard delete a registration.
|
|
116
|
+
|
|
117
|
+
Permanently removes the registration from storage.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
bot_id: Bot identifier
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if deleted, False if not found
|
|
124
|
+
"""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
async def deactivate(self, bot_id: str) -> bool:
|
|
128
|
+
"""Soft delete (set status to inactive).
|
|
129
|
+
|
|
130
|
+
Marks the registration as inactive without deleting.
|
|
131
|
+
Inactive registrations should not be returned by exists()
|
|
132
|
+
or list_active().
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
bot_id: Bot identifier
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if deactivated, False if not found
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
async def list_active(self) -> list[Registration]:
|
|
143
|
+
"""List all active registrations.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of active Registration objects
|
|
147
|
+
"""
|
|
148
|
+
...
|
|
149
|
+
|
|
150
|
+
async def list_all(self) -> list[Registration]:
|
|
151
|
+
"""List all registrations including inactive.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
List of all Registration objects
|
|
155
|
+
"""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
async def list_ids(self) -> list[str]:
|
|
159
|
+
"""List active bot IDs only.
|
|
160
|
+
|
|
161
|
+
More efficient than list_active() when only IDs are needed.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of active bot IDs
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
async def count(self) -> int:
|
|
169
|
+
"""Count active registrations.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Number of active registrations
|
|
173
|
+
"""
|
|
174
|
+
...
|
|
175
|
+
|
|
176
|
+
async def clear(self) -> None:
|
|
177
|
+
"""Clear all registrations.
|
|
178
|
+
|
|
179
|
+
Primarily useful for testing.
|
|
180
|
+
"""
|
|
181
|
+
...
|