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.
Files changed (42) hide show
  1. dataknobs_bots/__init__.py +42 -0
  2. dataknobs_bots/api/__init__.py +42 -0
  3. dataknobs_bots/api/dependencies.py +140 -0
  4. dataknobs_bots/api/exceptions.py +289 -0
  5. dataknobs_bots/bot/__init__.py +15 -0
  6. dataknobs_bots/bot/base.py +1091 -0
  7. dataknobs_bots/bot/context.py +102 -0
  8. dataknobs_bots/bot/manager.py +430 -0
  9. dataknobs_bots/bot/registry.py +629 -0
  10. dataknobs_bots/config/__init__.py +39 -0
  11. dataknobs_bots/config/resolution.py +353 -0
  12. dataknobs_bots/knowledge/__init__.py +82 -0
  13. dataknobs_bots/knowledge/query/__init__.py +25 -0
  14. dataknobs_bots/knowledge/query/expander.py +262 -0
  15. dataknobs_bots/knowledge/query/transformer.py +288 -0
  16. dataknobs_bots/knowledge/rag.py +738 -0
  17. dataknobs_bots/knowledge/retrieval/__init__.py +23 -0
  18. dataknobs_bots/knowledge/retrieval/formatter.py +249 -0
  19. dataknobs_bots/knowledge/retrieval/merger.py +279 -0
  20. dataknobs_bots/memory/__init__.py +56 -0
  21. dataknobs_bots/memory/base.py +38 -0
  22. dataknobs_bots/memory/buffer.py +58 -0
  23. dataknobs_bots/memory/vector.py +188 -0
  24. dataknobs_bots/middleware/__init__.py +11 -0
  25. dataknobs_bots/middleware/base.py +92 -0
  26. dataknobs_bots/middleware/cost.py +421 -0
  27. dataknobs_bots/middleware/logging.py +184 -0
  28. dataknobs_bots/reasoning/__init__.py +65 -0
  29. dataknobs_bots/reasoning/base.py +50 -0
  30. dataknobs_bots/reasoning/react.py +299 -0
  31. dataknobs_bots/reasoning/simple.py +51 -0
  32. dataknobs_bots/registry/__init__.py +41 -0
  33. dataknobs_bots/registry/backend.py +181 -0
  34. dataknobs_bots/registry/memory.py +244 -0
  35. dataknobs_bots/registry/models.py +102 -0
  36. dataknobs_bots/registry/portability.py +210 -0
  37. dataknobs_bots/tools/__init__.py +5 -0
  38. dataknobs_bots/tools/knowledge_search.py +113 -0
  39. dataknobs_bots/utils/__init__.py +1 -0
  40. dataknobs_bots-0.2.4.dist-info/METADATA +591 -0
  41. dataknobs_bots-0.2.4.dist-info/RECORD +42 -0
  42. 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
+ ...