busbot-memory 0.1.0__tar.gz

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.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: busbot-memory
3
+ Version: 0.1.0
4
+ Summary: LLM-powered working memory for Vietnamese bus booking bots
5
+ Author-email: QuocAnh <quocanhnguyen.work@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/biva-ai/busbot-memory
8
+ Project-URL: Documentation, https://github.com/biva-ai/busbot-memory#readme
9
+ Project-URL: Repository, https://github.com/biva-ai/busbot-memory.git
10
+ Project-URL: Issues, https://github.com/biva-ai/busbot-memory/issues
11
+ Keywords: llm,memory,chatbot,bus-booking,vietnamese,groq,voice-bot
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: groq>=0.4.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: redis
26
+ Requires-Dist: redis>=5.0.0; extra == "redis"
27
+ Provides-Extra: openai
28
+ Requires-Dist: openai>=1.0.0; extra == "openai"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Provides-Extra: all
34
+ Requires-Dist: redis>=5.0.0; extra == "all"
35
+ Requires-Dist: openai>=1.0.0; extra == "all"
36
+
37
+ # BusBotMemory SDK
38
+
39
+ LLM-powered working memory for Vietnamese bus booking bots.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install busbot-memory
45
+ ```
46
+
47
+ Or install from source:
48
+ ```bash
49
+ cd busbot-memory
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ import asyncio
57
+ from busbot_memory import BusBotMemory, BusBotConfig
58
+
59
+ async def main():
60
+ # Configure (set GROQ_API_KEY env var or pass directly)
61
+ config = BusBotConfig(groq_api_key="gsk_xxx")
62
+
63
+ # Initialize memory for a session
64
+ memory = BusBotMemory(
65
+ session_id="call_001",
66
+ customer_id="0987654321",
67
+ config=config
68
+ )
69
+
70
+ # Process messages
71
+ result = await memory.process("đặt 2 vé đi đà nẵng ngày mai 8h sáng")
72
+
73
+ print(result.state.slots)
74
+ # {"destination": "Đà Nẵng", "date": "ngày mai", "time": "08:00", "quantity": 2}
75
+
76
+ asyncio.run(main())
77
+ ```
78
+
79
+ ## Features
80
+
81
+ - **LLM-powered extraction**: Uses Groq (llama-3.3-70b) for accurate entity extraction
82
+ - **Change-of-mind detection**: Automatically detects when user changes their booking
83
+ - **State tracking**: Maintains structured booking state with missing slot tracking
84
+ - **Low latency**: Optimized for < 250ms processing time
85
+ - **Fallback support**: Falls back to regex when LLM is unavailable
86
+
87
+ ## Configuration
88
+
89
+ ```python
90
+ config = BusBotConfig(
91
+ # LLM Provider (at least one required)
92
+ groq_api_key="gsk_xxx", # Primary - fast & free
93
+ openai_api_key="sk-xxx", # Optional fallback
94
+
95
+ # Performance
96
+ latency_target_ms=250,
97
+ enable_fallback=True, # Use regex if LLM fails
98
+
99
+ # Memory settings
100
+ max_working_items=20,
101
+ max_context_window=5,
102
+ )
103
+ ```
104
+
105
+ ## ProcessResult
106
+
107
+ ```python
108
+ result = await memory.process(message)
109
+
110
+ result.entities # Extracted entities
111
+ result.state # BookingState object
112
+ result.is_noise # Is filler message ("ừ", "ok")
113
+ result.is_change # Did user change their mind
114
+ result.changes # List of changes made
115
+ result.intent # Detected intent
116
+ result.latency_ms # Processing time
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,85 @@
1
+ # BusBotMemory SDK
2
+
3
+ LLM-powered working memory for Vietnamese bus booking bots.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install busbot-memory
9
+ ```
10
+
11
+ Or install from source:
12
+ ```bash
13
+ cd busbot-memory
14
+ pip install -e .
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ import asyncio
21
+ from busbot_memory import BusBotMemory, BusBotConfig
22
+
23
+ async def main():
24
+ # Configure (set GROQ_API_KEY env var or pass directly)
25
+ config = BusBotConfig(groq_api_key="gsk_xxx")
26
+
27
+ # Initialize memory for a session
28
+ memory = BusBotMemory(
29
+ session_id="call_001",
30
+ customer_id="0987654321",
31
+ config=config
32
+ )
33
+
34
+ # Process messages
35
+ result = await memory.process("đặt 2 vé đi đà nẵng ngày mai 8h sáng")
36
+
37
+ print(result.state.slots)
38
+ # {"destination": "Đà Nẵng", "date": "ngày mai", "time": "08:00", "quantity": 2}
39
+
40
+ asyncio.run(main())
41
+ ```
42
+
43
+ ## Features
44
+
45
+ - **LLM-powered extraction**: Uses Groq (llama-3.3-70b) for accurate entity extraction
46
+ - **Change-of-mind detection**: Automatically detects when user changes their booking
47
+ - **State tracking**: Maintains structured booking state with missing slot tracking
48
+ - **Low latency**: Optimized for < 250ms processing time
49
+ - **Fallback support**: Falls back to regex when LLM is unavailable
50
+
51
+ ## Configuration
52
+
53
+ ```python
54
+ config = BusBotConfig(
55
+ # LLM Provider (at least one required)
56
+ groq_api_key="gsk_xxx", # Primary - fast & free
57
+ openai_api_key="sk-xxx", # Optional fallback
58
+
59
+ # Performance
60
+ latency_target_ms=250,
61
+ enable_fallback=True, # Use regex if LLM fails
62
+
63
+ # Memory settings
64
+ max_working_items=20,
65
+ max_context_window=5,
66
+ )
67
+ ```
68
+
69
+ ## ProcessResult
70
+
71
+ ```python
72
+ result = await memory.process(message)
73
+
74
+ result.entities # Extracted entities
75
+ result.state # BookingState object
76
+ result.is_noise # Is filler message ("ừ", "ok")
77
+ result.is_change # Did user change their mind
78
+ result.changes # List of changes made
79
+ result.intent # Detected intent
80
+ result.latency_ms # Processing time
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,15 @@
1
+ """BusBot Memory SDK - LLM-powered working memory for bus booking bots"""
2
+
3
+ from busbot_memory.core.manager import BusBotMemory
4
+ from busbot_memory.core.models import BookingState, MemoryItem, ProcessResult
5
+ from busbot_memory.core.config import BusBotConfig
6
+ from busbot_memory.version import __version__
7
+
8
+ __all__ = [
9
+ "BusBotMemory",
10
+ "BookingState",
11
+ "MemoryItem",
12
+ "ProcessResult",
13
+ "BusBotConfig",
14
+ "__version__",
15
+ ]
@@ -0,0 +1,21 @@
1
+ """Core module exports"""
2
+
3
+ from busbot_memory.core.models import (
4
+ BookingState,
5
+ MemoryItem,
6
+ MemoryMetadata,
7
+ ExtractionResult,
8
+ ProcessResult,
9
+ Intent,
10
+ )
11
+ from busbot_memory.core.config import BusBotConfig
12
+
13
+ __all__ = [
14
+ "BookingState",
15
+ "MemoryItem",
16
+ "MemoryMetadata",
17
+ "ExtractionResult",
18
+ "ProcessResult",
19
+ "Intent",
20
+ "BusBotConfig",
21
+ ]
@@ -0,0 +1,60 @@
1
+ """Configuration for BusBot Memory SDK"""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class BusBotConfig:
10
+ """
11
+ SDK Configuration
12
+
13
+ Example:
14
+ config = BusBotConfig(
15
+ groq_api_key="gsk_xxx",
16
+ redis_url="redis://localhost:6379",
17
+ domain="bus_booking"
18
+ )
19
+ """
20
+ # LLM Provider
21
+ groq_api_key: Optional[str] = field(
22
+ default_factory=lambda: os.getenv("GROQ_API_KEY")
23
+ )
24
+ groq_model: str = "llama-3.3-70b-versatile"
25
+ groq_fallback_model: str = "llama-3.1-8b-instant"
26
+
27
+ # OpenAI (optional, for higher quality)
28
+ openai_api_key: Optional[str] = field(
29
+ default_factory=lambda: os.getenv("OPENAI_API_KEY")
30
+ )
31
+ openai_model: str = "gpt-4o-mini"
32
+
33
+ # Storage
34
+ redis_url: Optional[str] = field(
35
+ default_factory=lambda: os.getenv("REDIS_URL")
36
+ )
37
+ session_ttl_seconds: int = 3600 # 1 hour
38
+ user_memory_ttl_days: int = 30 # 30 days
39
+
40
+ # Domain
41
+ domain: str = "bus_booking"
42
+
43
+ # Memory settings
44
+ max_working_items: int = 20
45
+ max_context_window: int = 5
46
+
47
+ # Performance
48
+ latency_target_ms: int = 250
49
+ enable_fallback: bool = True # Fallback to regex if LLM fails
50
+ enable_metrics: bool = True # Track latency metrics
51
+
52
+ # Logging
53
+ log_level: str = "INFO"
54
+ log_extractions: bool = False # Log LLM extraction results
55
+
56
+ def validate(self) -> bool:
57
+ """Validate configuration"""
58
+ if not self.groq_api_key and not self.openai_api_key:
59
+ raise ValueError("At least one of groq_api_key or openai_api_key must be set")
60
+ return True
@@ -0,0 +1,244 @@
1
+ """
2
+ BusBotMemory - Main SDK Entry Point
3
+
4
+ This is the primary class users will interact with.
5
+ """
6
+
7
+ import time
8
+ import logging
9
+ from typing import Optional, List
10
+ from collections import deque
11
+
12
+ from busbot_memory.core.config import BusBotConfig
13
+ from busbot_memory.core.models import (
14
+ BookingState,
15
+ MemoryItem,
16
+ MemoryMetadata,
17
+ ProcessResult,
18
+ ExtractionResult,
19
+ )
20
+ from busbot_memory.extractors.llm import LLMExtractor
21
+ from busbot_memory.extractors.regex import RegexExtractor
22
+ from busbot_memory.state.manager import StateManager
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class BusBotMemory:
28
+ """
29
+ LLM-powered working memory for bus booking bots
30
+
31
+ Example:
32
+ from busbot_memory import BusBotMemory, BusBotConfig
33
+
34
+ config = BusBotConfig(groq_api_key="gsk_xxx")
35
+ memory = BusBotMemory(
36
+ session_id="call_001",
37
+ customer_id="0987654321",
38
+ config=config
39
+ )
40
+
41
+ result = await memory.process("đặt 2 vé đi đà nẵng ngày mai")
42
+ print(result.state.slots) # {"destination": "Đà Nẵng", "quantity": 2, ...}
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ session_id: str,
48
+ customer_id: Optional[str] = None,
49
+ config: Optional[BusBotConfig] = None,
50
+ ):
51
+ self.session_id = session_id
52
+ self.customer_id = customer_id
53
+ self.config = config or BusBotConfig()
54
+
55
+ # Validate config
56
+ self.config.validate()
57
+
58
+ # Initialize components
59
+ self._llm_extractor = LLMExtractor(self.config)
60
+ self._regex_extractor = RegexExtractor()
61
+ self._state_manager = StateManager()
62
+
63
+ # Working memory storage
64
+ self._memory: deque = deque(maxlen=self.config.max_working_items)
65
+
66
+ # Booking state
67
+ self._state: BookingState = self._state_manager.create_initial_state()
68
+
69
+ # User memory (persistent info)
70
+ self._user_memory: dict = {}
71
+
72
+ # Metrics
73
+ self._latencies: List[int] = []
74
+
75
+ logger.info(f"BusBotMemory initialized: session={session_id}")
76
+
77
+ async def process(self, message: str, role: str = "user") -> ProcessResult:
78
+ """
79
+ Process a message and update memory + state
80
+
81
+ This is the main entry point for the SDK.
82
+
83
+ Args:
84
+ message: The message to process
85
+ role: "user" or "assistant"
86
+
87
+ Returns:
88
+ ProcessResult with entities, state, changes, and latency
89
+ """
90
+ start_time = time.perf_counter()
91
+
92
+ # Build context from recent memory
93
+ context = self._build_context()
94
+
95
+ # Extract entities using LLM (with fallback)
96
+ try:
97
+ extraction = await self._llm_extractor.extract(message, context)
98
+ except Exception as e:
99
+ logger.warning(f"LLM extraction failed: {e}")
100
+ if self.config.enable_fallback:
101
+ extraction = await self._regex_extractor.extract(message, context)
102
+ else:
103
+ raise
104
+
105
+ # Skip state update for noise messages
106
+ changes = []
107
+ if not extraction.is_noise:
108
+ # Update state
109
+ self._state, changes = self._state_manager.update(
110
+ self._state,
111
+ extraction
112
+ )
113
+
114
+ # Add to memory
115
+ self._add_to_memory(message, role, extraction)
116
+
117
+ # Extract user info if present
118
+ self._extract_user_info(extraction)
119
+
120
+ # Calculate latency
121
+ latency_ms = int((time.perf_counter() - start_time) * 1000)
122
+ self._latencies.append(latency_ms)
123
+
124
+ if self.config.enable_metrics:
125
+ logger.debug(f"Process latency: {latency_ms}ms")
126
+
127
+ return ProcessResult(
128
+ entities=extraction.entities,
129
+ state=self._state,
130
+ is_noise=extraction.is_noise,
131
+ is_change=extraction.is_change,
132
+ changes=changes,
133
+ intent=extraction.intent,
134
+ confidence=extraction.confidence,
135
+ latency_ms=latency_ms,
136
+ )
137
+
138
+ def _build_context(self) -> str:
139
+ """Build context string from recent memory"""
140
+ if not self._memory:
141
+ return "Đây là tin nhắn đầu tiên trong cuộc hội thoại."
142
+
143
+ recent = list(self._memory)[-self.config.max_context_window:]
144
+
145
+ lines = []
146
+ for item in recent:
147
+ role_label = "User" if item.role == "user" else "Bot"
148
+ lines.append(f"{role_label}: {item.content}")
149
+
150
+ # Add current state summary
151
+ if self._state.slots:
152
+ state_str = ", ".join(f"{k}={v}" for k, v in self._state.slots.items())
153
+ lines.append(f"Current booking: {state_str}")
154
+
155
+ return "\n".join(lines)
156
+
157
+ def _add_to_memory(
158
+ self,
159
+ message: str,
160
+ role: str,
161
+ extraction: ExtractionResult
162
+ ):
163
+ """Add message to working memory"""
164
+ item = MemoryItem(
165
+ content=message,
166
+ key=f"{role}_{len(self._memory)}",
167
+ role=role,
168
+ metadata=MemoryMetadata(
169
+ confidence=extraction.confidence,
170
+ tags=list(extraction.entities.keys()),
171
+ ),
172
+ )
173
+ self._memory.append(item)
174
+
175
+ def _extract_user_info(self, extraction: ExtractionResult):
176
+ """Extract and store user information"""
177
+ user_fields = ["customer_name", "customer_phone"]
178
+ for field in user_fields:
179
+ if field in extraction.entities:
180
+ self._user_memory[field] = extraction.entities[field]
181
+
182
+ # ========================================================================
183
+ # State Access
184
+ # ========================================================================
185
+
186
+ @property
187
+ def state(self) -> BookingState:
188
+ """Get current booking state"""
189
+ return self._state
190
+
191
+ @property
192
+ def memory(self) -> List[MemoryItem]:
193
+ """Get all memory items"""
194
+ return list(self._memory)
195
+
196
+ @property
197
+ def user_memory(self) -> dict:
198
+ """Get user persistent memory"""
199
+ return self._user_memory.copy()
200
+
201
+ # ========================================================================
202
+ # Metrics
203
+ # ========================================================================
204
+
205
+ def get_metrics(self) -> dict:
206
+ """Get performance metrics"""
207
+ if not self._latencies:
208
+ return {"count": 0}
209
+
210
+ sorted_latencies = sorted(self._latencies)
211
+
212
+ return {
213
+ "count": len(self._latencies),
214
+ "avg_ms": sum(self._latencies) // len(self._latencies),
215
+ "p50_ms": sorted_latencies[len(sorted_latencies) // 2],
216
+ "p95_ms": sorted_latencies[int(len(sorted_latencies) * 0.95)],
217
+ "max_ms": max(self._latencies),
218
+ }
219
+
220
+ # ========================================================================
221
+ # Serialization
222
+ # ========================================================================
223
+
224
+ def to_dict(self) -> dict:
225
+ """Export memory state for persistence"""
226
+ return {
227
+ "session_id": self.session_id,
228
+ "customer_id": self.customer_id,
229
+ "state": self._state.to_dict(),
230
+ "memory": [item.to_dict() for item in self._memory],
231
+ "user_memory": self._user_memory,
232
+ "metrics": self.get_metrics(),
233
+ }
234
+
235
+ def load_state(self, state_dict: dict):
236
+ """Load state from dict (e.g., from Redis)"""
237
+ if "state" in state_dict:
238
+ self._state = BookingState.from_dict(state_dict["state"])
239
+ if "user_memory" in state_dict:
240
+ self._user_memory = state_dict["user_memory"]
241
+ if "memory" in state_dict:
242
+ self._memory.clear()
243
+ for item_dict in state_dict["memory"]:
244
+ self._memory.append(MemoryItem.from_dict(item_dict))