mcp-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 +289 -0
- mcp_sequential_thinking/logging_conf.py +24 -0
- mcp_sequential_thinking/models.py +226 -0
- mcp_sequential_thinking/server.py +226 -0
- mcp_sequential_thinking/storage.py +253 -0
- mcp_sequential_thinking/storage_utils.py +323 -0
- mcp_sequential_thinking/utils.py +17 -0
- mcp_sequential_thinking-0.6.0.dist-info/METADATA +553 -0
- mcp_sequential_thinking-0.6.0.dist-info/RECORD +13 -0
- mcp_sequential_thinking-0.6.0.dist-info/WHEEL +4 -0
- mcp_sequential_thinking-0.6.0.dist-info/entry_points.txt +2 -0
- mcp_sequential_thinking-0.6.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from .logging_conf import configure_logging
|
|
5
|
+
from .models import ThoughtData, ThoughtStage
|
|
6
|
+
|
|
7
|
+
logger = configure_logging("sequential-thinking.analysis")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThoughtAnalyzer:
|
|
11
|
+
"""Analyzer for thought data to extract insights and patterns."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _is_mainline(thought: ThoughtData) -> bool:
|
|
15
|
+
"""Whether a thought belongs to the main line of reasoning.
|
|
16
|
+
|
|
17
|
+
Revisions and branch thoughts are excluded from progress metrics:
|
|
18
|
+
counting them would report e.g. 160% for a 5-thought session with
|
|
19
|
+
3 revisions.
|
|
20
|
+
"""
|
|
21
|
+
return not thought.is_revision and thought.branch_id is None
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def find_related_thoughts(
|
|
25
|
+
current_thought: ThoughtData, all_thoughts: List[ThoughtData], max_results: int = 3
|
|
26
|
+
) -> List[ThoughtData]:
|
|
27
|
+
"""Find thoughts related to the current thought.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
current_thought: The current thought to find related thoughts for
|
|
31
|
+
all_thoughts: All available thoughts to search through
|
|
32
|
+
max_results: Maximum number of related thoughts to return
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List[ThoughtData]: Related thoughts, sorted by relevance
|
|
36
|
+
"""
|
|
37
|
+
# First, find thoughts in the same stage
|
|
38
|
+
same_stage = [
|
|
39
|
+
t
|
|
40
|
+
for t in all_thoughts
|
|
41
|
+
if t.stage == current_thought.stage and t.id != current_thought.id
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Then, find thoughts with similar tags
|
|
45
|
+
if current_thought.tags:
|
|
46
|
+
tag_matches = []
|
|
47
|
+
for thought in all_thoughts:
|
|
48
|
+
if thought.id == current_thought.id:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# Count matching tags
|
|
52
|
+
matching_tags = set(current_thought.tags) & set(thought.tags)
|
|
53
|
+
if matching_tags:
|
|
54
|
+
tag_matches.append((thought, len(matching_tags)))
|
|
55
|
+
|
|
56
|
+
# Sort by number of matching tags (descending)
|
|
57
|
+
tag_matches.sort(key=lambda x: x[1], reverse=True)
|
|
58
|
+
tag_related = [t[0] for t in tag_matches]
|
|
59
|
+
else:
|
|
60
|
+
tag_related = []
|
|
61
|
+
|
|
62
|
+
# Combine and deduplicate results
|
|
63
|
+
combined = []
|
|
64
|
+
seen_ids = set()
|
|
65
|
+
|
|
66
|
+
# First add same stage thoughts
|
|
67
|
+
for thought in same_stage:
|
|
68
|
+
if thought.id not in seen_ids:
|
|
69
|
+
combined.append(thought)
|
|
70
|
+
seen_ids.add(thought.id)
|
|
71
|
+
|
|
72
|
+
if len(combined) >= max_results:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
# Then add tag-related thoughts
|
|
76
|
+
if len(combined) < max_results:
|
|
77
|
+
for thought in tag_related:
|
|
78
|
+
if thought.id not in seen_ids:
|
|
79
|
+
combined.append(thought)
|
|
80
|
+
seen_ids.add(thought.id)
|
|
81
|
+
|
|
82
|
+
if len(combined) >= max_results:
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
return combined
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def generate_summary(thoughts: List[ThoughtData]) -> Dict[str, Any]:
|
|
89
|
+
"""Generate a summary of the thinking process.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
thoughts: List of thoughts to summarize
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dict[str, Any]: Summary data
|
|
96
|
+
"""
|
|
97
|
+
if not thoughts:
|
|
98
|
+
return {"summary": "No thoughts recorded yet"}
|
|
99
|
+
|
|
100
|
+
# Group thoughts by stage
|
|
101
|
+
stages: Dict[str, List[ThoughtData]] = {}
|
|
102
|
+
for thought in thoughts:
|
|
103
|
+
if thought.stage.value not in stages:
|
|
104
|
+
stages[thought.stage.value] = []
|
|
105
|
+
stages[thought.stage.value].append(thought)
|
|
106
|
+
|
|
107
|
+
# Count tags - using a more readable approach with explicit steps
|
|
108
|
+
# Collect all tags from all thoughts
|
|
109
|
+
all_tags = []
|
|
110
|
+
for thought in thoughts:
|
|
111
|
+
all_tags.extend(thought.tags)
|
|
112
|
+
|
|
113
|
+
# Count occurrences of each tag
|
|
114
|
+
tag_counts = Counter(all_tags)
|
|
115
|
+
|
|
116
|
+
# Get the 5 most common tags
|
|
117
|
+
top_tags = tag_counts.most_common(5)
|
|
118
|
+
|
|
119
|
+
# Create summary
|
|
120
|
+
try:
|
|
121
|
+
# Progress is based on mainline thoughts only; revisions and
|
|
122
|
+
# branch thoughts don't advance the sequence.
|
|
123
|
+
mainline_thoughts = [t for t in thoughts if ThoughtAnalyzer._is_mainline(t)]
|
|
124
|
+
|
|
125
|
+
# Safely calculate max total thoughts to avoid division by zero
|
|
126
|
+
max_total = max((t.total_thoughts for t in mainline_thoughts), default=0)
|
|
127
|
+
|
|
128
|
+
# Calculate percent complete safely
|
|
129
|
+
percent_complete: float = 0.0
|
|
130
|
+
if max_total > 0:
|
|
131
|
+
percent_complete = (len(mainline_thoughts) / max_total) * 100
|
|
132
|
+
|
|
133
|
+
logger.debug(
|
|
134
|
+
f"Calculating completion: {len(mainline_thoughts)}/{max_total} "
|
|
135
|
+
f"= {percent_complete}%"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Build the summary dictionary with more readable and
|
|
139
|
+
# maintainable list comprehensions
|
|
140
|
+
|
|
141
|
+
# Count thoughts by stage
|
|
142
|
+
stage_counts = {stage: len(thoughts_list) for stage, thoughts_list in stages.items()}
|
|
143
|
+
|
|
144
|
+
# Create timeline entries
|
|
145
|
+
sorted_thoughts = sorted(thoughts, key=lambda x: x.thought_number)
|
|
146
|
+
timeline_entries = []
|
|
147
|
+
for t in sorted_thoughts:
|
|
148
|
+
entry: Dict[str, Any] = {"number": t.thought_number, "stage": t.stage.value}
|
|
149
|
+
if t.is_revision:
|
|
150
|
+
entry["isRevision"] = True
|
|
151
|
+
if t.branch_id is not None:
|
|
152
|
+
entry["branchId"] = t.branch_id
|
|
153
|
+
timeline_entries.append(entry)
|
|
154
|
+
|
|
155
|
+
# Aggregate branches: first occurrence defines the fork point.
|
|
156
|
+
branches: Dict[str, Dict[str, Any]] = {}
|
|
157
|
+
for t in sorted_thoughts:
|
|
158
|
+
if t.branch_id is None:
|
|
159
|
+
continue
|
|
160
|
+
if t.branch_id not in branches:
|
|
161
|
+
branches[t.branch_id] = {
|
|
162
|
+
"fromThought": t.branch_from_thought,
|
|
163
|
+
"thoughtCount": 0,
|
|
164
|
+
}
|
|
165
|
+
branches[t.branch_id]["thoughtCount"] += 1
|
|
166
|
+
|
|
167
|
+
revision_count = sum(1 for t in thoughts if t.is_revision)
|
|
168
|
+
|
|
169
|
+
# Create top tags entries
|
|
170
|
+
top_tags_entries = []
|
|
171
|
+
for tag, count in top_tags:
|
|
172
|
+
top_tags_entries.append({"tag": tag, "count": count})
|
|
173
|
+
|
|
174
|
+
# Check if all stages are represented
|
|
175
|
+
all_stages_present = all(stage.value in stages for stage in ThoughtStage)
|
|
176
|
+
|
|
177
|
+
# Assemble the final summary
|
|
178
|
+
summary = {
|
|
179
|
+
"totalThoughts": len(thoughts),
|
|
180
|
+
"stages": stage_counts,
|
|
181
|
+
"timeline": timeline_entries,
|
|
182
|
+
"branches": branches,
|
|
183
|
+
"revisionCount": revision_count,
|
|
184
|
+
"topTags": top_tags_entries,
|
|
185
|
+
"completionStatus": {
|
|
186
|
+
"hasAllStages": all_stages_present,
|
|
187
|
+
"percentComplete": percent_complete,
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error generating summary: {e}")
|
|
192
|
+
summary = {"totalThoughts": len(thoughts), "error": str(e)}
|
|
193
|
+
|
|
194
|
+
return {"summary": summary}
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def analyze_thought(thought: ThoughtData, all_thoughts: List[ThoughtData]) -> Dict[str, Any]:
|
|
198
|
+
"""Analyze a single thought in the context of all thoughts.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
thought: The thought to analyze
|
|
202
|
+
all_thoughts: All available thoughts for context
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dict[str, Any]: Analysis results
|
|
206
|
+
"""
|
|
207
|
+
# Find related thoughts
|
|
208
|
+
related_thoughts = ThoughtAnalyzer.find_related_thoughts(thought, all_thoughts)
|
|
209
|
+
|
|
210
|
+
# Check if this is the first thought in its stage (lowest thought_number)
|
|
211
|
+
same_stage_thoughts = [t for t in all_thoughts if t.stage == thought.stage]
|
|
212
|
+
is_first_in_stage = all(
|
|
213
|
+
t.thought_number >= thought.thought_number for t in same_stage_thoughts
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Calculate progress. Revisions and branch thoughts don't advance the
|
|
217
|
+
# sequence, so for them progress reflects the mainline position instead
|
|
218
|
+
# of their own number (which may exceed total_thoughts).
|
|
219
|
+
if ThoughtAnalyzer._is_mainline(thought):
|
|
220
|
+
effective_number = thought.thought_number
|
|
221
|
+
else:
|
|
222
|
+
effective_number = max(
|
|
223
|
+
(t.thought_number for t in all_thoughts if ThoughtAnalyzer._is_mainline(t)),
|
|
224
|
+
default=0,
|
|
225
|
+
)
|
|
226
|
+
progress = (effective_number / thought.total_thoughts) * 100
|
|
227
|
+
|
|
228
|
+
# For a revision, surface a snippet of the mainline thought it revises.
|
|
229
|
+
revision_of = None
|
|
230
|
+
if thought.is_revision and thought.revises_thought_number is not None:
|
|
231
|
+
revised = next(
|
|
232
|
+
(
|
|
233
|
+
t
|
|
234
|
+
for t in all_thoughts
|
|
235
|
+
if ThoughtAnalyzer._is_mainline(t)
|
|
236
|
+
and t.thought_number == thought.revises_thought_number
|
|
237
|
+
),
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
if revised is not None:
|
|
241
|
+
revision_of = {
|
|
242
|
+
"thoughtNumber": revised.thought_number,
|
|
243
|
+
"stage": revised.stage.value,
|
|
244
|
+
"snippet": (
|
|
245
|
+
revised.thought[:100] + "..."
|
|
246
|
+
if len(revised.thought) > 100
|
|
247
|
+
else revised.thought
|
|
248
|
+
),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Create analysis
|
|
252
|
+
analysis_block: Dict[str, Any] = {
|
|
253
|
+
"relatedThoughtsCount": len(related_thoughts),
|
|
254
|
+
"relatedThoughtSummaries": [
|
|
255
|
+
{
|
|
256
|
+
"thoughtNumber": t.thought_number,
|
|
257
|
+
"stage": t.stage.value,
|
|
258
|
+
"snippet": (
|
|
259
|
+
t.thought[:100] + "..." if len(t.thought) > 100 else t.thought
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
for t in related_thoughts
|
|
263
|
+
],
|
|
264
|
+
"progress": progress,
|
|
265
|
+
"isFirstInStage": is_first_in_stage,
|
|
266
|
+
"isRevision": thought.is_revision,
|
|
267
|
+
"revisedThought": thought.revises_thought_number,
|
|
268
|
+
"branchId": thought.branch_id,
|
|
269
|
+
}
|
|
270
|
+
if revision_of is not None:
|
|
271
|
+
analysis_block["revisionOf"] = revision_of
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
"thoughtAnalysis": {
|
|
275
|
+
"currentThought": {
|
|
276
|
+
"thoughtNumber": thought.thought_number,
|
|
277
|
+
"totalThoughts": thought.total_thoughts,
|
|
278
|
+
"nextThoughtNeeded": thought.next_thought_needed,
|
|
279
|
+
"stage": thought.stage.value,
|
|
280
|
+
"tags": thought.tags,
|
|
281
|
+
"timestamp": thought.timestamp,
|
|
282
|
+
},
|
|
283
|
+
"analysis": analysis_block,
|
|
284
|
+
"context": {
|
|
285
|
+
"thoughtHistoryLength": len(all_thoughts),
|
|
286
|
+
"currentStage": thought.stage.value,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def configure_logging(name: str = "sequential-thinking") -> logging.Logger:
|
|
6
|
+
"""Configure and return a logger with standardized settings.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
name: The name for the logger
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
logging.Logger: Configured logger instance
|
|
13
|
+
"""
|
|
14
|
+
# Configure root logger
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
18
|
+
handlers=[
|
|
19
|
+
logging.StreamHandler(sys.stderr)
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Get and return the named logger
|
|
24
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from uuid import uuid4, UUID
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationInfo
|
|
7
|
+
|
|
8
|
+
# branch_id ends up in files and tool output, so it is restricted to a short,
|
|
9
|
+
# filesystem- and log-safe alphabet.
|
|
10
|
+
BRANCH_ID_MAX_LENGTH = 64
|
|
11
|
+
BRANCH_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThoughtStage(Enum):
|
|
15
|
+
"""Basic thinking stages for structured sequential thinking."""
|
|
16
|
+
PROBLEM_DEFINITION = "Problem Definition"
|
|
17
|
+
RESEARCH = "Research"
|
|
18
|
+
ANALYSIS = "Analysis"
|
|
19
|
+
SYNTHESIS = "Synthesis"
|
|
20
|
+
CONCLUSION = "Conclusion"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_string(cls, value: str) -> 'ThoughtStage':
|
|
24
|
+
"""Convert a string to a thinking stage.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
value: The string representation of the thinking stage
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
ThoughtStage: The corresponding ThoughtStage enum value
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the string does not match any valid thinking stage
|
|
34
|
+
"""
|
|
35
|
+
# Case-insensitive comparison
|
|
36
|
+
for stage in cls:
|
|
37
|
+
if stage.value.casefold() == value.casefold():
|
|
38
|
+
return stage
|
|
39
|
+
|
|
40
|
+
# If no match found
|
|
41
|
+
valid_stages = ", ".join(stage.value for stage in cls)
|
|
42
|
+
raise ValueError(f"Invalid thinking stage: '{value}'. Valid stages are: {valid_stages}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ThoughtData(BaseModel):
|
|
46
|
+
"""Data structure for a single thought in the sequential thinking process."""
|
|
47
|
+
thought: str
|
|
48
|
+
thought_number: int
|
|
49
|
+
total_thoughts: int
|
|
50
|
+
next_thought_needed: bool
|
|
51
|
+
stage: ThoughtStage
|
|
52
|
+
tags: List[str] = Field(default_factory=list)
|
|
53
|
+
axioms_used: List[str] = Field(default_factory=list)
|
|
54
|
+
assumptions_challenged: List[str] = Field(default_factory=list)
|
|
55
|
+
is_revision: bool = False
|
|
56
|
+
revises_thought_number: Optional[int] = None
|
|
57
|
+
branch_from_thought: Optional[int] = None
|
|
58
|
+
branch_id: Optional[str] = None
|
|
59
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
60
|
+
id: UUID = Field(default_factory=uuid4)
|
|
61
|
+
|
|
62
|
+
def __hash__(self) -> int:
|
|
63
|
+
"""Make ThoughtData hashable based on its ID."""
|
|
64
|
+
return hash(self.id)
|
|
65
|
+
|
|
66
|
+
def __eq__(self, other: object) -> bool:
|
|
67
|
+
"""Compare ThoughtData objects based on their ID."""
|
|
68
|
+
if not isinstance(other, ThoughtData):
|
|
69
|
+
return False
|
|
70
|
+
return self.id == other.id
|
|
71
|
+
|
|
72
|
+
@field_validator('thought')
|
|
73
|
+
@classmethod
|
|
74
|
+
def thought_not_empty(cls, v: str) -> str:
|
|
75
|
+
"""Validate that thought content is not empty."""
|
|
76
|
+
if not v or not v.strip():
|
|
77
|
+
raise ValueError("Thought content cannot be empty")
|
|
78
|
+
return v
|
|
79
|
+
|
|
80
|
+
@field_validator('thought_number')
|
|
81
|
+
@classmethod
|
|
82
|
+
def thought_number_positive(cls, v: int) -> int:
|
|
83
|
+
"""Validate that thought number is positive."""
|
|
84
|
+
if v < 1:
|
|
85
|
+
raise ValueError("Thought number must be positive")
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
@field_validator('total_thoughts')
|
|
89
|
+
@classmethod
|
|
90
|
+
def total_thoughts_valid(cls, v: int, info: ValidationInfo) -> int:
|
|
91
|
+
"""Validate that total thoughts is valid."""
|
|
92
|
+
thought_number = info.data.get('thought_number')
|
|
93
|
+
if thought_number is not None and v < thought_number:
|
|
94
|
+
raise ValueError("Total thoughts must be greater or equal to current thought number")
|
|
95
|
+
return v
|
|
96
|
+
|
|
97
|
+
@model_validator(mode='after')
|
|
98
|
+
def validate_revision_and_branch(self) -> 'ThoughtData':
|
|
99
|
+
"""Validate the cross-field rules for revisions and branches."""
|
|
100
|
+
if self.is_revision and self.revises_thought_number is None:
|
|
101
|
+
raise ValueError("is_revision=True requires revises_thought_number to be set")
|
|
102
|
+
if self.revises_thought_number is not None and not self.is_revision:
|
|
103
|
+
raise ValueError("revises_thought_number requires is_revision=True")
|
|
104
|
+
if self.is_revision and self.branch_from_thought is not None:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"A thought cannot be a revision and a branch start at the same time"
|
|
107
|
+
)
|
|
108
|
+
if self.branch_id is not None and self.branch_from_thought is None:
|
|
109
|
+
raise ValueError("branch_id requires branch_from_thought to be set")
|
|
110
|
+
|
|
111
|
+
for field_name, value in (
|
|
112
|
+
("revises_thought_number", self.revises_thought_number),
|
|
113
|
+
("branch_from_thought", self.branch_from_thought),
|
|
114
|
+
):
|
|
115
|
+
if value is not None and not 1 <= value < self.thought_number:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"{field_name} must be >= 1 and < thought_number "
|
|
118
|
+
f"({self.thought_number}), got {value}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if self.branch_id is not None and (
|
|
122
|
+
len(self.branch_id) > BRANCH_ID_MAX_LENGTH
|
|
123
|
+
or not BRANCH_ID_PATTERN.match(self.branch_id)
|
|
124
|
+
):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"branch_id must be 1-{BRANCH_ID_MAX_LENGTH} characters from "
|
|
127
|
+
"[A-Za-z0-9_-]"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def to_dict(self, include_id: bool = False) -> dict:
|
|
133
|
+
"""Convert the thought data to a dictionary representation.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
include_id: Whether to include the ID in the dictionary representation.
|
|
137
|
+
Default is False to omit it from external representations.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
dict: Dictionary representation of the thought data, with camelCase
|
|
141
|
+
keys for API consistency.
|
|
142
|
+
"""
|
|
143
|
+
result = {
|
|
144
|
+
"thought": self.thought,
|
|
145
|
+
"thoughtNumber": self.thought_number,
|
|
146
|
+
"totalThoughts": self.total_thoughts,
|
|
147
|
+
"nextThoughtNeeded": self.next_thought_needed,
|
|
148
|
+
"stage": self.stage.value,
|
|
149
|
+
"tags": self.tags,
|
|
150
|
+
"axiomsUsed": self.axioms_used,
|
|
151
|
+
"assumptionsChallenged": self.assumptions_challenged,
|
|
152
|
+
"timestamp": self.timestamp,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Revision/branch fields are only emitted when set, keeping records
|
|
156
|
+
# compact and older v2 files readable without them.
|
|
157
|
+
if self.is_revision:
|
|
158
|
+
result["isRevision"] = self.is_revision
|
|
159
|
+
if self.revises_thought_number is not None:
|
|
160
|
+
result["revisesThoughtNumber"] = self.revises_thought_number
|
|
161
|
+
if self.branch_from_thought is not None:
|
|
162
|
+
result["branchFromThought"] = self.branch_from_thought
|
|
163
|
+
if self.branch_id is not None:
|
|
164
|
+
result["branchId"] = self.branch_id
|
|
165
|
+
|
|
166
|
+
if include_id:
|
|
167
|
+
result["id"] = str(self.id)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_dict(cls, data: dict) -> 'ThoughtData':
|
|
173
|
+
"""Create a ThoughtData instance from a dictionary.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
data: Dictionary containing thought data
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
ThoughtData: A new ThoughtData instance
|
|
180
|
+
"""
|
|
181
|
+
# Convert any camelCase keys to snake_case
|
|
182
|
+
snake_data = {}
|
|
183
|
+
mappings = {
|
|
184
|
+
"thoughtNumber": "thought_number",
|
|
185
|
+
"totalThoughts": "total_thoughts",
|
|
186
|
+
"nextThoughtNeeded": "next_thought_needed",
|
|
187
|
+
"axiomsUsed": "axioms_used",
|
|
188
|
+
"assumptionsChallenged": "assumptions_challenged",
|
|
189
|
+
"isRevision": "is_revision",
|
|
190
|
+
"revisesThoughtNumber": "revises_thought_number",
|
|
191
|
+
"branchFromThought": "branch_from_thought",
|
|
192
|
+
"branchId": "branch_id"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Process known direct mappings
|
|
196
|
+
for camel_key, snake_key in mappings.items():
|
|
197
|
+
if camel_key in data:
|
|
198
|
+
snake_data[snake_key] = data[camel_key]
|
|
199
|
+
|
|
200
|
+
# Copy fields that don't need conversion
|
|
201
|
+
for key in ["thought", "tags", "timestamp"]:
|
|
202
|
+
if key in data:
|
|
203
|
+
snake_data[key] = data[key]
|
|
204
|
+
|
|
205
|
+
# Handle special fields
|
|
206
|
+
if "stage" in data:
|
|
207
|
+
snake_data["stage"] = ThoughtStage.from_string(data["stage"])
|
|
208
|
+
|
|
209
|
+
# Set default values for missing fields
|
|
210
|
+
snake_data.setdefault("tags", [])
|
|
211
|
+
snake_data.setdefault("axioms_used", data.get("axiomsUsed", []))
|
|
212
|
+
snake_data.setdefault("assumptions_challenged", data.get("assumptionsChallenged", []))
|
|
213
|
+
snake_data.setdefault("timestamp", datetime.now().isoformat())
|
|
214
|
+
|
|
215
|
+
# Add ID if present, otherwise generate a new one
|
|
216
|
+
if "id" in data:
|
|
217
|
+
try:
|
|
218
|
+
snake_data["id"] = UUID(data["id"])
|
|
219
|
+
except (ValueError, TypeError):
|
|
220
|
+
snake_data["id"] = uuid4()
|
|
221
|
+
|
|
222
|
+
return cls(**snake_data)
|
|
223
|
+
|
|
224
|
+
model_config = {
|
|
225
|
+
"arbitrary_types_allowed": True
|
|
226
|
+
}
|