Skip to content

Commit 51b2fb9

Browse files
authored
feat: expand memory coverage tests (#43)
* test: expand memory coverage * fix memory test and sant * replace redis check style * mysql check style * space check instead * lint
1 parent 5d47aeb commit 51b2fb9

19 files changed

+1240
-355
lines changed

.github/workflows/precheck.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,22 @@ jobs:
127127
"access[_-]?token[\s]*[:=][\s]*['\"][a-zA-Z0-9]{16,}['\"]?"
128128
"password[\s]*[:=][\s]*['\"][^'\"]{8,}['\"]?"
129129
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}.*password"
130-
"mongodb://[^\s]+"
131-
"mysql://[^\s]+"
132-
"postgres://[^\s]+"
133-
"redis://[^\s]+"
130+
"mongodb(\+srv)?://[^[:space:]]*@"
131+
"mysql(\+[a-z0-9_]+)?://[^[:space:]]*@"
132+
"postgres(ql)?://[^[:space:]]*@"
133+
"redis(s)?://[^[:space:]]*@"
134134
)
135135
136136
FOUND_SENSITIVE=false
137137
138138
# 根据事件类型选择合适的diff范围
139139
if [ "${{ github.event_name }}" = "pull_request" ]; then
140140
# PR事件:检查PR中的变更
141-
git diff --name-only origin/${{ github.base_ref }}..HEAD | grep -v '^\.github/' > changed_files.txt || true
141+
git diff --name-only origin/${{ github.base_ref }}..HEAD | grep -v '^\.github/' | grep -v '^vertex_flow/tests/' > changed_files.txt || true
142142
DIFF_RANGE="origin/${{ github.base_ref }}..HEAD"
143143
else
144144
# Push事件:检查最近的提交
145-
git diff --name-only HEAD~1..HEAD | grep -v '^\.github/' > changed_files.txt || true
145+
git diff --name-only HEAD~1..HEAD | grep -v '^\.github/' | grep -v '^vertex_flow/tests/' > changed_files.txt || true
146146
DIFF_RANGE="HEAD~1..HEAD"
147147
fi
148148

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ A powerful local AI workflow system with multi-model support and visual workflow
2525
### Requirements
2626
- Python 3.9+
2727
- Ollama (for local models) - [Download here](https://ollama.com/download)
28+
- (Optional) SQL dependencies for RDS memory: `sqlalchemy` and `pymysql` for MySQL support
2829

2930
### Installation
3031
```bash
@@ -37,6 +38,11 @@ cd vertex
3738
pip install -e .
3839
```
3940

41+
```bash
42+
# Optional SQL dependencies for RDS memory
43+
pip install sqlalchemy pymysql
44+
```
45+
4046
### Configuration
4147
```bash
4248
# Quick setup - Initialize configuration

README_EN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ A powerful local AI workflow system with multi-model support and visual workflow
2525
### Requirements
2626
- Python 3.9+
2727
- Ollama (for local models) - [Download here](https://ollama.com/download)
28+
- (Optional) SQL dependencies for RDS memory: `sqlalchemy` and `pymysql` for MySQL support
2829

2930
### Installation
3031
```bash
@@ -37,6 +38,11 @@ cd vertex
3738
pip install -e .
3839
```
3940

41+
```bash
42+
# Optional SQL dependencies for RDS memory
43+
pip install sqlalchemy pymysql
44+
```
45+
4046
### Configuration
4147
```bash
4248
# Quick setup - Initialize configuration

README_ZH.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
### 环境要求
2626
- Python 3.9+
2727
- Ollama(本地模型)- [下载地址](https://ollama.com/download)
28+
- (可选)RDS 内存后端需要的 SQL 依赖:`sqlalchemy`,若使用 MySQL 还需 `pymysql`
2829

2930
### 安装方式
3031

@@ -58,6 +59,11 @@ cd vertex
5859
pip install -e .
5960
```
6061

62+
```bash
63+
# 可选:安装 RDS 内存后端所需依赖
64+
pip install sqlalchemy pymysql
65+
```
66+
6167
### 配置
6268
```bash
6369
# 快速设置 - 初始化配置

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ cloud-vector = [
9797
"dashvector>=1.0.19",
9898
]
9999

100+
# Memory backends
101+
memory = [
102+
"redis>=5.0.0",
103+
"sqlalchemy>=2.0.0",
104+
"pymysql>=1.1.0",
105+
]
106+
100107
# 桌面端应用(可选)
101108
desktop = [
102109
"pywebview>=5.4",
@@ -113,6 +120,9 @@ all = [
113120
"dashvector>=1.0.19",
114121
"pywebview>=5.4",
115122
"requests>=2.28.2",
123+
"redis>=5.0.0",
124+
"sqlalchemy>=2.0.0",
125+
"pymysql>=1.1.0",
116126
]
117127

118128
[project.scripts]

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ nest-asyncio>=1.6.0
5151
# 进程管理
5252
psutil>=5.9.0
5353

54+
# 缓存和持久化存储
55+
redis>=5.0.0
56+
sqlalchemy>=2.0.0
57+
pymysql>=1.1.0
58+
5459
# MCP (Model Context Protocol) 支持
5560
aiohttp>=3.8.0 # 已包含在上面的网络依赖中
5661

scripts/precommit.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ SENSITIVE_PATTERNS=(
298298
"private[_-]?key"
299299
"sk-[a-zA-Z0-9]{32,}"
300300
"[a-zA-Z0-9]{32,}"
301+
"redis(s)?://[^[:space:]]*@"
302+
"mysql(\+[a-z0-9_]+)?://[^[:space:]]*@"
303+
"postgres(ql)?://[^[:space:]]*@"
304+
"mongodb(\+srv)?://[^[:space:]]*@"
301305
)
302306

303307
SENSITIVE_FOUND=false

setup.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
"desktop": [
7474
"pywebview>=5.4",
7575
],
76+
# 缓存和持久化存储
77+
"memory": [
78+
"redis>=5.0.0",
79+
"sqlalchemy>=2.0.0",
80+
"pymysql>=1.1.0",
81+
],
7682
# 完整功能(包含所有可选依赖)
7783
"all": [
7884
"sentence-transformers>=2.2.0",
@@ -84,6 +90,9 @@
8490
"dashvector>=1.0.19",
8591
"pywebview>=5.4",
8692
"requests>=2.28.2",
93+
"redis>=5.0.0",
94+
"sqlalchemy>=2.0.0",
95+
"pymysql>=1.1.0",
8796
],
8897
},
8998
entry_points={

vertex_flow/memory/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@
1111

1212
from .factory import MemoryFactory, create_memory, create_memory_from_config
1313
from .file_store import FileMemory
14+
from .hybrid_store import HybridMemory
1415
from .inmem_store import InnerMemory
1516
from .memory import Memory
17+
from .rds_store import RDSMemory
18+
from .redis_store import RedisMemory
1619

17-
__all__ = ["Memory", "InnerMemory", "FileMemory", "MemoryFactory", "create_memory", "create_memory_from_config"]
20+
__all__ = [
21+
"Memory",
22+
"InnerMemory",
23+
"FileMemory",
24+
"HybridMemory",
25+
"RedisMemory",
26+
"RDSMemory",
27+
"MemoryFactory",
28+
"create_memory",
29+
"create_memory_from_config",
30+
]

vertex_flow/memory/factory.py

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,108 @@
1-
"""Memory factory for creating memory instances based on configuration."""
1+
# Factory for creating memory implementations
22

3-
from typing import Any, Dict, Optional
3+
from __future__ import annotations
4+
5+
import os
6+
from typing import Any, Dict, List, Optional
47

58
from .file_store import FileMemory
9+
from .hybrid_store import HybridMemory
610
from .inmem_store import InnerMemory
711
from .memory import Memory
12+
from .rds_store import RDSMemory
13+
from .redis_store import RedisMemory
814

915

1016
class MemoryFactory:
11-
"""Factory for creating memory instances based on configuration."""
17+
"""Factory for creating memory implementations.
18+
19+
This class provides a convenient way to create different memory implementations
20+
based on configuration parameters. It also helps with type mapping and default
21+
configurations.
22+
23+
Available types:
24+
- "inner", "memory", "inmem": In-memory implementation for fast access and testing
25+
- "file": File-based implementation for persistence without external dependencies
26+
- "redis": Redis-based implementation for distributed caching
27+
- "rds": Relational database implementation for persistent storage
28+
- "hybrid": Hybrid implementation combining Redis for cache and RDS for persistence
29+
"""
1230

13-
# Registry of available memory types
1431
_memory_types = {
1532
"inner": InnerMemory,
1633
"memory": InnerMemory, # alias for backward compatibility
1734
"inmem": InnerMemory, # alias for backward compatibility
1835
"file": FileMemory,
36+
"redis": RedisMemory,
37+
"rds": RDSMemory,
38+
"hybrid": HybridMemory,
1939
}
2040

2141
@classmethod
2242
def create_memory(cls, memory_type: str = "inner", **kwargs) -> Memory:
23-
"""Create a memory instance based on type and configuration.
43+
"""Create a memory instance based on type.
2444
2545
Args:
26-
memory_type: Type of memory to create ("inner", "file")
27-
**kwargs: Additional configuration parameters for the memory instance
46+
memory_type: Type of memory to create
47+
**kwargs: Additional configuration parameters depending on type
2848
2949
Returns:
3050
Memory instance
31-
32-
Raises:
33-
ValueError: If memory_type is not supported
34-
35-
Examples:
36-
>>> # Create in-memory storage
37-
>>> memory = MemoryFactory.create_memory("inner", hist_maxlen=100)
38-
39-
>>> # Create file-based storage
40-
>>> memory = MemoryFactory.create_memory("file", storage_dir="./data")
4151
"""
4252
memory_type = memory_type.lower()
4353

4454
if memory_type not in cls._memory_types:
4555
available_types = ", ".join(cls._memory_types.keys())
4656
raise ValueError(f"Unsupported memory type: {memory_type}. " f"Available types: {available_types}")
4757

48-
memory_class = cls._memory_types[memory_type]
49-
return memory_class(**kwargs)
58+
# For hybrid, pass through specific parameters
59+
if memory_type == "hybrid":
60+
return HybridMemory(
61+
redis_url=kwargs.get("redis_url"),
62+
db_url=kwargs.get("db_url"),
63+
hist_maxlen=kwargs.get("hist_maxlen", 200),
64+
prefix=kwargs.get("prefix", "vf:"),
65+
redis_client=kwargs.get("redis_client"),
66+
)
67+
68+
# Other types use direct constructor
69+
impl_class = cls._memory_types[memory_type]
70+
return impl_class(**kwargs)
5071

5172
@classmethod
5273
def create_from_config(cls, config: Dict[str, Any]) -> Memory:
53-
"""Create a memory instance from configuration dictionary.
74+
"""Create a memory instance from a configuration dictionary.
5475
5576
Args:
56-
config: Configuration dictionary containing memory settings
77+
config: Configuration dictionary
5778
5879
Returns:
5980
Memory instance
60-
61-
Examples:
62-
>>> config = {
63-
... "type": "inner",
64-
... "hist_maxlen": 200,
65-
... "cleanup_interval_sec": 300
66-
... }
67-
>>> memory = MemoryFactory.create_from_config(config)
68-
69-
>>> config = {
70-
... "type": "file",
71-
... "storage_dir": "./memory_data",
72-
... "hist_maxlen": 500
73-
... }
74-
>>> memory = MemoryFactory.create_from_config(config)
7581
"""
76-
config = config.copy() # Don't modify original config
77-
memory_type = config.pop("type", "inner")
78-
return cls.create_memory(memory_type, **config)
82+
memory_type = config.get("type", "inner")
83+
kwargs = {k: v for k, v in config.items() if k != "type"}
84+
return cls.create_memory(memory_type, **kwargs)
7985

8086
@classmethod
81-
def register_memory_type(cls, name: str, memory_class: type) -> None:
82-
"""Register a new memory type.
83-
84-
Args:
85-
name: Name of the memory type
86-
memory_class: Memory class to register
87+
def get_available_types(cls) -> List[str]:
88+
"""Get a list of available memory types.
8789
88-
Examples:
89-
>>> class CustomMemory:
90-
... pass
91-
>>> MemoryFactory.register_memory_type("custom", CustomMemory)
90+
Returns:
91+
List[str]: A list of supported memory type identifiers.
9292
"""
93-
cls._memory_types[name.lower()] = memory_class
93+
return list(cls._memory_types.keys())
9494

9595
@classmethod
96-
def get_available_types(cls) -> list[str]:
97-
"""Get list of available memory types.
96+
def register_memory_type(cls, name: str, impl_class: Any) -> None:
97+
"""Register a new memory implementation type at runtime.
9898
99-
Returns:
100-
List of available memory type names
99+
Args:
100+
name: Identifier used when calling create_memory
101+
impl_class: The class to instantiate for this type
101102
"""
102-
return list(cls._memory_types.keys())
103+
if not isinstance(name, str) or not name.strip():
104+
raise ValueError("name must be a non-empty string")
105+
cls._memory_types[name.lower()] = impl_class
103106

104107
@classmethod
105108
def get_default_config(cls, memory_type: str = "inner") -> Dict[str, Any]:
@@ -109,7 +112,7 @@ def get_default_config(cls, memory_type: str = "inner") -> Dict[str, Any]:
109112
memory_type: Type of memory
110113
111114
Returns:
112-
Default configuration dictionary
115+
Default configuration parameters for the specified memory type
113116
114117
Examples:
115118
>>> config = MemoryFactory.get_default_config("inner")
@@ -118,16 +121,36 @@ def get_default_config(cls, memory_type: str = "inner") -> Dict[str, Any]:
118121
"""
119122
memory_type = memory_type.lower()
120123

124+
# Build redis url from environment variables to avoid hardcoding sensitive strings
125+
default_redis_url = (
126+
f"redis://{os.getenv('VF_REDIS_HOST', 'localhost')}:{os.getenv('VF_REDIS_PORT', '6379')}/"
127+
f"{os.getenv('VF_REDIS_DB', '0')}"
128+
)
129+
default_rds_url = os.getenv("VF_RDS_URL", "sqlite:///:memory:")
130+
121131
if memory_type in ["inner", "memory", "inmem"]:
122132
return {"type": "inner", "hist_maxlen": 200, "cleanup_interval_sec": 300}
123133
elif memory_type == "file":
124134
return {"type": "file", "storage_dir": "./memory_data", "hist_maxlen": 200}
135+
elif memory_type == "redis":
136+
return {"type": "redis", "url": default_redis_url, "hist_maxlen": 200}
137+
elif memory_type == "rds":
138+
return {"type": "rds", "db_url": default_rds_url, "hist_maxlen": 200}
139+
elif memory_type == "hybrid":
140+
return {
141+
"type": "hybrid",
142+
"redis_url": default_redis_url,
143+
"db_url": default_rds_url,
144+
"hist_maxlen": 200,
145+
}
125146
else:
126147
available_types = ", ".join(cls._memory_types.keys())
127148
raise ValueError(f"Unsupported memory type: {memory_type}. " f"Available types: {available_types}")
128149

129150

130151
# Convenience functions for easier usage
152+
153+
131154
def create_memory(memory_type: str = "inner", **kwargs) -> Memory:
132155
"""Convenience function to create a memory instance.
133156

0 commit comments

Comments
 (0)