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.
- busbot_memory-0.1.0/PKG-INFO +121 -0
- busbot_memory-0.1.0/README.md +85 -0
- busbot_memory-0.1.0/busbot_memory/__init__.py +15 -0
- busbot_memory-0.1.0/busbot_memory/core/__init__.py +21 -0
- busbot_memory-0.1.0/busbot_memory/core/config.py +60 -0
- busbot_memory-0.1.0/busbot_memory/core/manager.py +244 -0
- busbot_memory-0.1.0/busbot_memory/core/models.py +193 -0
- busbot_memory-0.1.0/busbot_memory/domains/__init__.py +1 -0
- busbot_memory-0.1.0/busbot_memory/extractors/__init__.py +7 -0
- busbot_memory-0.1.0/busbot_memory/extractors/base.py +27 -0
- busbot_memory-0.1.0/busbot_memory/extractors/llm.py +197 -0
- busbot_memory-0.1.0/busbot_memory/extractors/regex.py +128 -0
- busbot_memory-0.1.0/busbot_memory/memory/__init__.py +1 -0
- busbot_memory-0.1.0/busbot_memory/state/__init__.py +5 -0
- busbot_memory-0.1.0/busbot_memory/state/manager.py +90 -0
- busbot_memory-0.1.0/busbot_memory/storage/__init__.py +1 -0
- busbot_memory-0.1.0/busbot_memory/utils/__init__.py +1 -0
- busbot_memory-0.1.0/busbot_memory/version.py +2 -0
- busbot_memory-0.1.0/busbot_memory.egg-info/PKG-INFO +121 -0
- busbot_memory-0.1.0/busbot_memory.egg-info/SOURCES.txt +23 -0
- busbot_memory-0.1.0/busbot_memory.egg-info/dependency_links.txt +1 -0
- busbot_memory-0.1.0/busbot_memory.egg-info/requires.txt +17 -0
- busbot_memory-0.1.0/busbot_memory.egg-info/top_level.txt +1 -0
- busbot_memory-0.1.0/pyproject.toml +56 -0
- busbot_memory-0.1.0/setup.cfg +4 -0
|
@@ -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))
|