sequential-thinking 0.6.0__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.
- mcp_sequential_thinking/__init__.py +0 -0
- mcp_sequential_thinking/analysis.py +219 -0
- mcp_sequential_thinking/logging_conf.py +22 -0
- mcp_sequential_thinking/models.py +200 -0
- mcp_sequential_thinking/server.py +213 -0
- mcp_sequential_thinking/storage.py +152 -0
- mcp_sequential_thinking/storage_utils.py +107 -0
- mcp_sequential_thinking/utils.py +71 -0
- sequential_thinking-0.6.0.dist-info/METADATA +475 -0
- sequential_thinking-0.6.0.dist-info/RECORD +13 -0
- sequential_thinking-0.6.0.dist-info/WHEEL +4 -0
- sequential_thinking-0.6.0.dist-info/entry_points.txt +2 -0
- sequential_thinking-0.6.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .logging_conf import configure_logging
|
|
6
|
+
from .models import ThoughtData, ThoughtStage
|
|
7
|
+
from .storage_utils import (
|
|
8
|
+
load_thoughts_from_file,
|
|
9
|
+
prepare_thoughts_for_serialization,
|
|
10
|
+
save_thoughts_to_file,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = configure_logging("sequential-thinking.storage")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ThoughtStorage:
|
|
17
|
+
"""Storage manager for thought data."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, storage_dir: str | None = None):
|
|
20
|
+
"""Initialize the storage manager.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
storage_dir: Directory to store thought data files. If None, uses a default directory.
|
|
24
|
+
"""
|
|
25
|
+
if storage_dir is None:
|
|
26
|
+
# Use user's home directory by default
|
|
27
|
+
home_dir = Path.home()
|
|
28
|
+
self.storage_dir = home_dir / ".mcp_sequential_thinking"
|
|
29
|
+
else:
|
|
30
|
+
self.storage_dir = Path(storage_dir)
|
|
31
|
+
|
|
32
|
+
# Create storage directory if it doesn't exist
|
|
33
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
# Default session file
|
|
36
|
+
self.current_session_file = self.storage_dir / "current_session.json"
|
|
37
|
+
self.lock_file = self.storage_dir / "current_session.lock"
|
|
38
|
+
|
|
39
|
+
# Thread safety
|
|
40
|
+
self._lock = threading.RLock()
|
|
41
|
+
self.thought_history: list[ThoughtData] = []
|
|
42
|
+
|
|
43
|
+
# Load existing session if available
|
|
44
|
+
self._load_session()
|
|
45
|
+
|
|
46
|
+
def _load_session(self) -> None:
|
|
47
|
+
"""Load thought history from the current session file if it exists."""
|
|
48
|
+
with self._lock:
|
|
49
|
+
# Use the utility function to handle loading with proper error handling
|
|
50
|
+
self.thought_history = load_thoughts_from_file(
|
|
51
|
+
self.current_session_file, self.lock_file
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _save_session(self) -> None:
|
|
55
|
+
"""Save the current thought history to the session file."""
|
|
56
|
+
# Use thread lock to ensure consistent data
|
|
57
|
+
with self._lock:
|
|
58
|
+
# Use utility functions to prepare and save thoughts
|
|
59
|
+
thoughts_with_ids = prepare_thoughts_for_serialization(self.thought_history)
|
|
60
|
+
|
|
61
|
+
# Save to file with proper locking
|
|
62
|
+
save_thoughts_to_file(self.current_session_file, thoughts_with_ids, self.lock_file)
|
|
63
|
+
|
|
64
|
+
def add_thought(self, thought: ThoughtData) -> None:
|
|
65
|
+
"""Add a thought to the history and save the session.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
thought: The thought data to add
|
|
69
|
+
"""
|
|
70
|
+
with self._lock:
|
|
71
|
+
self.thought_history.append(thought)
|
|
72
|
+
self._save_session()
|
|
73
|
+
|
|
74
|
+
def get_all_thoughts(self) -> list[ThoughtData]:
|
|
75
|
+
"""Get all thoughts in the current session.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List[ThoughtData]: All thoughts in the current session
|
|
79
|
+
"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
# Return a copy to avoid external modification
|
|
82
|
+
return list(self.thought_history)
|
|
83
|
+
|
|
84
|
+
def get_thoughts_by_stage(self, stage: ThoughtStage) -> list[ThoughtData]:
|
|
85
|
+
"""Get all thoughts in a specific stage.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
stage: The thinking stage to filter by
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List[ThoughtData]: Thoughts in the specified stage
|
|
92
|
+
"""
|
|
93
|
+
with self._lock:
|
|
94
|
+
return [t for t in self.thought_history if t.stage == stage]
|
|
95
|
+
|
|
96
|
+
def clear_history(self) -> None:
|
|
97
|
+
"""Clear the thought history and save the empty session."""
|
|
98
|
+
with self._lock:
|
|
99
|
+
self.thought_history.clear()
|
|
100
|
+
self._save_session()
|
|
101
|
+
|
|
102
|
+
def export_session(self, file_path: str) -> None:
|
|
103
|
+
"""Export the current session to a file.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
file_path: Path to save the exported session
|
|
107
|
+
"""
|
|
108
|
+
with self._lock:
|
|
109
|
+
# Use utility function to prepare thoughts for serialization
|
|
110
|
+
thoughts_with_ids = prepare_thoughts_for_serialization(self.thought_history)
|
|
111
|
+
|
|
112
|
+
# Create export-specific metadata
|
|
113
|
+
metadata = {
|
|
114
|
+
"exportedAt": datetime.now().isoformat(),
|
|
115
|
+
"metadata": {
|
|
116
|
+
"totalThoughts": len(self.thought_history),
|
|
117
|
+
"stages": {
|
|
118
|
+
stage.value: len([t for t in self.thought_history if t.stage == stage])
|
|
119
|
+
for stage in ThoughtStage
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Convert string path to Path object for compatibility with utility
|
|
125
|
+
file_path_obj = Path(file_path)
|
|
126
|
+
lock_file = file_path_obj.with_suffix(".lock")
|
|
127
|
+
|
|
128
|
+
# Use utility function to save with proper locking
|
|
129
|
+
save_thoughts_to_file(file_path_obj, thoughts_with_ids, lock_file, metadata)
|
|
130
|
+
|
|
131
|
+
def import_session(self, file_path: str) -> None:
|
|
132
|
+
"""Import a session from a file.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
file_path: Path to the file to import
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
FileNotFoundError: If the file doesn't exist
|
|
139
|
+
json.JSONDecodeError: If the file is not valid JSON
|
|
140
|
+
KeyError: If the file doesn't contain valid thought data
|
|
141
|
+
"""
|
|
142
|
+
# Convert string path to Path object for compatibility with utility
|
|
143
|
+
file_path_obj = Path(file_path)
|
|
144
|
+
lock_file = file_path_obj.with_suffix(".lock")
|
|
145
|
+
|
|
146
|
+
# Use utility function to load thoughts with proper error handling
|
|
147
|
+
thoughts = load_thoughts_from_file(file_path_obj, lock_file)
|
|
148
|
+
|
|
149
|
+
with self._lock:
|
|
150
|
+
self.thought_history = thoughts
|
|
151
|
+
|
|
152
|
+
self._save_session()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
import portalocker
|
|
7
|
+
|
|
8
|
+
from .logging_conf import configure_logging
|
|
9
|
+
from .models import ThoughtData
|
|
10
|
+
|
|
11
|
+
logger = configure_logging("sequential-thinking.storage-utils")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prepare_thoughts_for_serialization(thoughts: list[ThoughtData]) -> list[dict[str, Any]]:
|
|
15
|
+
"""Prepare thoughts for serialization with IDs included.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
thoughts: List of thought data objects to prepare
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List[Dict[str, Any]]: List of thought dictionaries with IDs
|
|
22
|
+
"""
|
|
23
|
+
return [thought.to_dict(include_id=True) for thought in thoughts]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_thoughts_to_file(
|
|
27
|
+
file_path: Path,
|
|
28
|
+
thoughts: list[dict[str, Any]],
|
|
29
|
+
lock_file: Path,
|
|
30
|
+
metadata: dict[str, Any] | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Save thoughts to a file with proper locking.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
file_path: Path to the file to save
|
|
36
|
+
thoughts: List of thought dictionaries to save
|
|
37
|
+
lock_file: Path to the lock file
|
|
38
|
+
metadata: Optional additional metadata to include
|
|
39
|
+
"""
|
|
40
|
+
data: dict[str, Any] = {"thoughts": thoughts, "lastUpdated": datetime.now().isoformat()}
|
|
41
|
+
|
|
42
|
+
# Add any additional metadata if provided
|
|
43
|
+
if metadata:
|
|
44
|
+
data.update(metadata)
|
|
45
|
+
|
|
46
|
+
# Ensure destination directories exist before acquiring the lock.
|
|
47
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Use file locking to ensure thread safety when writing
|
|
51
|
+
with portalocker.Lock(lock_file, timeout=10), open(file_path, "w", encoding="utf-8") as f:
|
|
52
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
53
|
+
|
|
54
|
+
logger.debug(f"Saved {len(thoughts)} thoughts to {file_path}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_thoughts_from_file(file_path: Path, lock_file: Path) -> list[ThoughtData]:
|
|
58
|
+
"""Load thoughts from a file with proper locking.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: Path to the file to load
|
|
62
|
+
lock_file: Path to the lock file
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List[ThoughtData]: Loaded thought data objects
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
json.JSONDecodeError: If the file is not valid JSON
|
|
69
|
+
KeyError: If the file doesn't contain valid thought data
|
|
70
|
+
"""
|
|
71
|
+
if not file_path.exists():
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Use file locking and file handling in a single with statement
|
|
76
|
+
# for cleaner resource management
|
|
77
|
+
with portalocker.Lock(lock_file, timeout=10), open(file_path, encoding="utf-8") as f:
|
|
78
|
+
data = json.load(f)
|
|
79
|
+
|
|
80
|
+
if not isinstance(data, dict):
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
data_dict = cast(dict[str, Any], data)
|
|
84
|
+
raw_thoughts = data_dict.get("thoughts", [])
|
|
85
|
+
if not isinstance(raw_thoughts, list):
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
thought_dicts = cast(list[Any], raw_thoughts)
|
|
89
|
+
|
|
90
|
+
# Convert data to ThoughtData objects after file is closed
|
|
91
|
+
thoughts = [
|
|
92
|
+
ThoughtData.from_dict(cast(dict[str, Any], thought_dict))
|
|
93
|
+
for thought_dict in thought_dicts
|
|
94
|
+
if isinstance(thought_dict, dict)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Loaded {len(thoughts)} thoughts from {file_path}")
|
|
98
|
+
return thoughts
|
|
99
|
+
|
|
100
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
101
|
+
# Handle corrupted file
|
|
102
|
+
logger.error(f"Error loading from {file_path}: {e}")
|
|
103
|
+
# Create backup of corrupted file
|
|
104
|
+
backup_file = file_path.with_suffix(f".bak.{datetime.now().strftime('%Y%m%d%H%M%S')}")
|
|
105
|
+
file_path.rename(backup_file)
|
|
106
|
+
logger.info(f"Created backup of corrupted file at {backup_file}")
|
|
107
|
+
return []
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Utility functions for the sequential thinking package.
|
|
2
|
+
|
|
3
|
+
This module contains common utilities used across the package.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_camel_case(snake_str: str) -> str:
|
|
12
|
+
"""Convert a snake_case string to camelCase.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
snake_str: A string in snake_case format
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The string converted to camelCase
|
|
19
|
+
"""
|
|
20
|
+
components = snake_str.split("_")
|
|
21
|
+
# Join with the first component lowercase and the rest with their first letter capitalized
|
|
22
|
+
return components[0] + "".join(x.title() for x in components[1:])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_snake_case(camel_str: str) -> str:
|
|
26
|
+
"""Convert a camelCase string to snake_case.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
camel_str: A string in camelCase format
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The string converted to snake_case
|
|
33
|
+
"""
|
|
34
|
+
# Insert underscore before uppercase letters and convert to lowercase
|
|
35
|
+
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", camel_str)
|
|
36
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def convert_dict_keys(data: Any, converter: Callable[[str], str]) -> Any:
|
|
40
|
+
"""Convert all keys in a dictionary using the provided converter function.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
data: Dictionary with keys to convert
|
|
44
|
+
converter: Function to convert the keys (e.g. to_camel_case or to_snake_case)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A new dictionary with converted keys
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(data, dict):
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
source = cast(dict[str, Any], data)
|
|
53
|
+
result: dict[str, Any] = {}
|
|
54
|
+
for key, value in source.items():
|
|
55
|
+
# Convert key
|
|
56
|
+
new_key = converter(key)
|
|
57
|
+
|
|
58
|
+
# If value is a dict, recursively convert its keys too
|
|
59
|
+
if isinstance(value, dict):
|
|
60
|
+
result[new_key] = convert_dict_keys(value, converter)
|
|
61
|
+
# If value is a list, check if items are dicts and convert them
|
|
62
|
+
elif isinstance(value, list):
|
|
63
|
+
items = cast(list[Any], value)
|
|
64
|
+
result[new_key] = [
|
|
65
|
+
convert_dict_keys(item, converter) if isinstance(item, dict) else item
|
|
66
|
+
for item in items
|
|
67
|
+
]
|
|
68
|
+
else:
|
|
69
|
+
result[new_key] = value
|
|
70
|
+
|
|
71
|
+
return result
|