zrb 1.9.17__py3-none-any.whl → 1.10.1__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.
- zrb/__init__.py +2 -2
- zrb/builtin/llm/history.py +3 -5
- zrb/builtin/llm/tool/cli.py +17 -13
- zrb/builtin/llm/tool/file.py +2 -2
- zrb/builtin/llm/tool/sub_agent.py +3 -5
- zrb/config/config.py +8 -12
- zrb/config/llm_config.py +132 -107
- zrb/config/llm_rate_limitter.py +13 -2
- zrb/task/llm/conversation_history.py +128 -0
- zrb/task/llm/conversation_history_model.py +438 -0
- zrb/task/llm/history_summarization.py +76 -26
- zrb/task/llm/prompt.py +106 -14
- zrb/task/llm_task.py +53 -92
- zrb/util/llm/prompt.py +18 -0
- {zrb-1.9.17.dist-info → zrb-1.10.1.dist-info}/METADATA +1 -1
- {zrb-1.9.17.dist-info → zrb-1.10.1.dist-info}/RECORD +18 -18
- zrb/task/llm/context.py +0 -58
- zrb/task/llm/context_enrichment.py +0 -172
- zrb/task/llm/history.py +0 -233
- {zrb-1.9.17.dist-info → zrb-1.10.1.dist-info}/WHEEL +0 -0
- {zrb-1.9.17.dist-info → zrb-1.10.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
import json
|
2
|
+
from collections.abc import Callable
|
3
|
+
from copy import deepcopy
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from zrb.attr.type import StrAttr
|
7
|
+
from zrb.context.any_context import AnyContext
|
8
|
+
from zrb.context.any_shared_context import AnySharedContext
|
9
|
+
from zrb.task.llm.conversation_history_model import ConversationHistory
|
10
|
+
from zrb.task.llm.typing import ListOfDict
|
11
|
+
from zrb.util.attr import get_str_attr
|
12
|
+
from zrb.util.file import write_file
|
13
|
+
from zrb.util.run import run_async
|
14
|
+
|
15
|
+
|
16
|
+
def get_history_file(
|
17
|
+
ctx: AnyContext,
|
18
|
+
conversation_history_file_attr: StrAttr | None,
|
19
|
+
render_history_file: bool,
|
20
|
+
) -> str:
|
21
|
+
"""Gets the path to the conversation history file, rendering if configured."""
|
22
|
+
return get_str_attr(
|
23
|
+
ctx,
|
24
|
+
conversation_history_file_attr,
|
25
|
+
"",
|
26
|
+
auto_render=render_history_file,
|
27
|
+
)
|
28
|
+
|
29
|
+
|
30
|
+
async def read_conversation_history(
|
31
|
+
ctx: AnyContext,
|
32
|
+
conversation_history_reader: (
|
33
|
+
Callable[[AnySharedContext], ConversationHistory | dict | list | None] | None
|
34
|
+
),
|
35
|
+
conversation_history_file_attr: StrAttr | None,
|
36
|
+
render_history_file: bool,
|
37
|
+
conversation_history_attr: (
|
38
|
+
ConversationHistory
|
39
|
+
| Callable[[AnySharedContext], ConversationHistory | dict | list]
|
40
|
+
| dict
|
41
|
+
| list
|
42
|
+
),
|
43
|
+
) -> ConversationHistory:
|
44
|
+
"""Reads conversation history from reader, file, or attribute, with validation."""
|
45
|
+
history_file = get_history_file(
|
46
|
+
ctx, conversation_history_file_attr, render_history_file
|
47
|
+
)
|
48
|
+
# Use the class method defined above
|
49
|
+
history_data = await ConversationHistory.read_from_source(
|
50
|
+
ctx=ctx,
|
51
|
+
reader=conversation_history_reader,
|
52
|
+
file_path=history_file,
|
53
|
+
)
|
54
|
+
if history_data:
|
55
|
+
return history_data
|
56
|
+
# Priority 3: Callable or direct conversation_history attribute
|
57
|
+
raw_data_attr: Any = None
|
58
|
+
if callable(conversation_history_attr):
|
59
|
+
try:
|
60
|
+
raw_data_attr = await run_async(conversation_history_attr(ctx))
|
61
|
+
except Exception as e:
|
62
|
+
ctx.log_warning(
|
63
|
+
f"Error executing callable conversation_history attribute: {e}. "
|
64
|
+
"Ignoring."
|
65
|
+
)
|
66
|
+
if raw_data_attr is None:
|
67
|
+
raw_data_attr = conversation_history_attr
|
68
|
+
if raw_data_attr:
|
69
|
+
# Use the class method defined above
|
70
|
+
history_data = ConversationHistory.parse_and_validate(
|
71
|
+
ctx, raw_data_attr, "attribute"
|
72
|
+
)
|
73
|
+
if history_data:
|
74
|
+
return history_data
|
75
|
+
# Fallback: Return default value
|
76
|
+
return ConversationHistory()
|
77
|
+
|
78
|
+
|
79
|
+
async def write_conversation_history(
|
80
|
+
ctx: AnyContext,
|
81
|
+
history_data: ConversationHistory,
|
82
|
+
conversation_history_writer: (
|
83
|
+
Callable[[AnySharedContext, ConversationHistory], None] | None
|
84
|
+
),
|
85
|
+
conversation_history_file_attr: StrAttr | None,
|
86
|
+
render_history_file: bool,
|
87
|
+
):
|
88
|
+
"""Writes conversation history using the writer or to a file."""
|
89
|
+
if conversation_history_writer is not None:
|
90
|
+
await run_async(conversation_history_writer(ctx, history_data))
|
91
|
+
history_file = get_history_file(
|
92
|
+
ctx, conversation_history_file_attr, render_history_file
|
93
|
+
)
|
94
|
+
if history_file != "":
|
95
|
+
write_file(history_file, json.dumps(history_data.to_dict(), indent=2))
|
96
|
+
|
97
|
+
|
98
|
+
def replace_system_prompt_in_history(
|
99
|
+
history_list: ListOfDict, replacement: str = "<main LLM system prompt>"
|
100
|
+
) -> ListOfDict:
|
101
|
+
"""
|
102
|
+
Returns a new history list where any part with part_kind 'system-prompt'
|
103
|
+
has its 'content' replaced with the given replacement string.
|
104
|
+
Args:
|
105
|
+
history: List of history items (each item is a dict with a 'parts' list).
|
106
|
+
replacement: The string to use in place of system-prompt content.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
A deep-copied list of history items with system-prompt content replaced.
|
110
|
+
"""
|
111
|
+
new_history = deepcopy(history_list)
|
112
|
+
for item in new_history:
|
113
|
+
parts = item.get("parts", [])
|
114
|
+
for part in parts:
|
115
|
+
if part.get("part_kind") == "system-prompt":
|
116
|
+
part["content"] = replacement
|
117
|
+
return new_history
|
118
|
+
|
119
|
+
|
120
|
+
def count_part_in_history_list(history_list: ListOfDict) -> int:
|
121
|
+
"""Calculates the total number of 'parts' in a history list."""
|
122
|
+
history_part_len = 0
|
123
|
+
for history in history_list:
|
124
|
+
if "parts" in history:
|
125
|
+
history_part_len += len(history["parts"])
|
126
|
+
else:
|
127
|
+
history_part_len += 1
|
128
|
+
return history_part_len
|
@@ -0,0 +1,438 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from zrb.config.config import CFG
|
7
|
+
from zrb.context.any_context import AnyContext
|
8
|
+
from zrb.task.llm.typing import ListOfDict
|
9
|
+
from zrb.util.file import read_file, read_file_with_line_numbers, write_file
|
10
|
+
from zrb.util.run import run_async
|
11
|
+
|
12
|
+
|
13
|
+
class ConversationHistory:
|
14
|
+
|
15
|
+
def __init__(
|
16
|
+
self,
|
17
|
+
past_conversation_summary: str = "",
|
18
|
+
past_conversation_transcript: str = "",
|
19
|
+
history: ListOfDict | None = None,
|
20
|
+
contextual_note: str | None = None,
|
21
|
+
long_term_note: str | None = None,
|
22
|
+
project_path: str | None = None,
|
23
|
+
):
|
24
|
+
self.past_conversation_transcript = past_conversation_transcript
|
25
|
+
self.past_conversation_summary = past_conversation_summary
|
26
|
+
self.history = history if history is not None else []
|
27
|
+
self.contextual_note = contextual_note if contextual_note is not None else ""
|
28
|
+
self.long_term_note = long_term_note if long_term_note is not None else ""
|
29
|
+
self.project_path = project_path if project_path is not None else os.getcwd()
|
30
|
+
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
32
|
+
return {
|
33
|
+
"past_conversation_summary": self.past_conversation_summary,
|
34
|
+
"past_conversation_transcript": self.past_conversation_transcript,
|
35
|
+
"history": self.history,
|
36
|
+
"contextual_note": self.contextual_note,
|
37
|
+
"long_term_note": self.long_term_note,
|
38
|
+
}
|
39
|
+
|
40
|
+
def model_dump_json(self, indent: int = 2) -> str:
|
41
|
+
return json.dumps(self.to_dict(), indent=indent)
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
async def read_from_source(
|
45
|
+
cls,
|
46
|
+
ctx: AnyContext,
|
47
|
+
reader: Callable[[AnyContext], dict[str, Any] | list | None] | None,
|
48
|
+
file_path: str | None,
|
49
|
+
) -> "ConversationHistory | None":
|
50
|
+
# Priority 1: Reader function
|
51
|
+
if reader:
|
52
|
+
try:
|
53
|
+
raw_data = await run_async(reader(ctx))
|
54
|
+
if raw_data:
|
55
|
+
instance = cls.parse_and_validate(ctx, raw_data, "reader")
|
56
|
+
if instance:
|
57
|
+
return instance
|
58
|
+
except Exception as e:
|
59
|
+
ctx.log_warning(
|
60
|
+
f"Error executing conversation history reader: {e}. Ignoring."
|
61
|
+
)
|
62
|
+
# Priority 2: History file
|
63
|
+
if file_path and os.path.isfile(file_path):
|
64
|
+
try:
|
65
|
+
content = read_file(file_path)
|
66
|
+
raw_data = json.loads(content)
|
67
|
+
instance = cls.parse_and_validate(ctx, raw_data, f"file '{file_path}'")
|
68
|
+
if instance:
|
69
|
+
return instance
|
70
|
+
except json.JSONDecodeError:
|
71
|
+
ctx.log_warning(
|
72
|
+
f"Could not decode JSON from history file '{file_path}'. "
|
73
|
+
"Ignoring file content."
|
74
|
+
)
|
75
|
+
except Exception as e:
|
76
|
+
ctx.log_warning(
|
77
|
+
f"Error reading history file '{file_path}': {e}. "
|
78
|
+
"Ignoring file content."
|
79
|
+
)
|
80
|
+
# Fallback: Return default value
|
81
|
+
return None
|
82
|
+
|
83
|
+
def fetch_newest_notes(self):
|
84
|
+
long_term_note_path = self._get_long_term_note_path()
|
85
|
+
if os.path.isfile(long_term_note_path):
|
86
|
+
self.long_term_note = read_file(long_term_note_path)
|
87
|
+
contextual_note_path = self._get_contextual_note_path()
|
88
|
+
if os.path.isfile(contextual_note_path):
|
89
|
+
self.contextual_note = read_file(contextual_note_path)
|
90
|
+
|
91
|
+
@classmethod
|
92
|
+
def parse_and_validate(
|
93
|
+
cls, ctx: AnyContext, data: Any, source: str
|
94
|
+
) -> "ConversationHistory":
|
95
|
+
try:
|
96
|
+
if isinstance(data, cls):
|
97
|
+
return data # Already a valid instance
|
98
|
+
if isinstance(data, dict):
|
99
|
+
# This handles both the new format and the old {'context': ..., 'history': ...}
|
100
|
+
return cls(
|
101
|
+
past_conversation_summary=data.get("past_conversation_summary", ""),
|
102
|
+
past_conversation_transcript=data.get(
|
103
|
+
"past_conversation_transcript", ""
|
104
|
+
),
|
105
|
+
history=data.get("history", data.get("messages", [])),
|
106
|
+
contextual_note=data.get("contextual_note", ""),
|
107
|
+
long_term_note=data.get("long_term_note", ""),
|
108
|
+
)
|
109
|
+
elif isinstance(data, list):
|
110
|
+
# Handle very old format (just a list) - wrap it
|
111
|
+
ctx.log_warning(
|
112
|
+
f"History from {source} contains legacy list format. "
|
113
|
+
"Wrapping it into the new structure. "
|
114
|
+
"Consider updating the source format."
|
115
|
+
)
|
116
|
+
return cls(history=data)
|
117
|
+
else:
|
118
|
+
ctx.log_warning(
|
119
|
+
f"History data from {source} has unexpected format "
|
120
|
+
f"(type: {type(data)}). Ignoring."
|
121
|
+
)
|
122
|
+
except Exception as e: # Catch validation errors too
|
123
|
+
ctx.log_warning(
|
124
|
+
f"Error validating/parsing history data from {source}: {e}. Ignoring."
|
125
|
+
)
|
126
|
+
return cls()
|
127
|
+
|
128
|
+
def write_past_conversation_summary(self, past_conversation_summary: str):
|
129
|
+
"""
|
130
|
+
Write or update the past conversation summary.
|
131
|
+
|
132
|
+
Use this tool to store or update a summary of previous conversations for
|
133
|
+
future reference. This is useful for providing context to LLMs or other tools
|
134
|
+
that need a concise history.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
past_conversation_summary (str): The summary text to store.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
None
|
141
|
+
|
142
|
+
Raises:
|
143
|
+
Exception: If the summary cannot be written.
|
144
|
+
"""
|
145
|
+
self.past_conversation_summary = past_conversation_summary
|
146
|
+
|
147
|
+
def write_past_conversation_transcript(self, past_conversation_transcript: str):
|
148
|
+
"""
|
149
|
+
Write or update the past conversation transcript.
|
150
|
+
|
151
|
+
Use this tool to store or update the full transcript of previous conversations.
|
152
|
+
This is useful for providing detailed context to LLMs or for record-keeping.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
past_conversation_transcript (str): The transcript text to store.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
None
|
159
|
+
|
160
|
+
Raises:
|
161
|
+
Exception: If the transcript cannot be written.
|
162
|
+
"""
|
163
|
+
self.past_conversation_transcript = past_conversation_transcript
|
164
|
+
|
165
|
+
def read_long_term_note(
|
166
|
+
self,
|
167
|
+
start_line: int | None = None,
|
168
|
+
end_line: int | None = None,
|
169
|
+
) -> str:
|
170
|
+
"""
|
171
|
+
Read the content of the long-term note, optionally for a specific line range.
|
172
|
+
|
173
|
+
This tool helps you retrieve knowledge or notes stored for long-term reference.
|
174
|
+
If the note does not exist, you may want to create it using the write tool.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
start_line (int, optional): 1-based line number to start reading from.
|
178
|
+
end_line (int, optional): 1-based line number to stop reading at (inclusive).
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
str: JSON with file path, content (with line numbers), start/end lines,
|
182
|
+
and total lines.
|
183
|
+
|
184
|
+
Raises:
|
185
|
+
Exception: If the note cannot be read.
|
186
|
+
Suggests writing the note if it does not exist.
|
187
|
+
"""
|
188
|
+
return self._read_note(
|
189
|
+
self._get_long_term_note_path(),
|
190
|
+
start_line,
|
191
|
+
end_line,
|
192
|
+
note_type="long-term note",
|
193
|
+
)
|
194
|
+
|
195
|
+
def write_long_term_note(self, content: str) -> str:
|
196
|
+
"""
|
197
|
+
Write or overwrite the content of the long-term note.
|
198
|
+
|
199
|
+
Use this tool to create a new long-term note or replace its entire content.
|
200
|
+
Always read the note first to avoid accidental data loss, unless you are sure
|
201
|
+
you want to overwrite.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
content (str): The full content to write to the note.
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
str: JSON indicating success and the note path.
|
208
|
+
|
209
|
+
Raises:
|
210
|
+
Exception: If the note cannot be written. Suggests checking permissions or path.
|
211
|
+
"""
|
212
|
+
self.long_term_note = content
|
213
|
+
return self._write_note(
|
214
|
+
self._get_long_term_note_path(), content, note_type="long-term note"
|
215
|
+
)
|
216
|
+
|
217
|
+
def replace_in_long_term_note(
|
218
|
+
self,
|
219
|
+
old_string: str,
|
220
|
+
new_string: str,
|
221
|
+
) -> str:
|
222
|
+
"""
|
223
|
+
Replace the first occurrence of a string in the long-term note.
|
224
|
+
|
225
|
+
Use this tool to update a specific part of the long-term note without
|
226
|
+
overwriting the entire content. If the note does not exist, consider writing it
|
227
|
+
first. If the string is not found, check your input or read the note to verify.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
old_string (str): The exact string to search for and replace.
|
231
|
+
new_string (str): The string to replace with.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
str: JSON indicating success and the note path.
|
235
|
+
|
236
|
+
Raises:
|
237
|
+
Exception: If the note does not exist or the string is not found.
|
238
|
+
Suggests writing or reading the note.
|
239
|
+
"""
|
240
|
+
result = self._replace_in_note(
|
241
|
+
self._get_long_term_note_path(),
|
242
|
+
old_string,
|
243
|
+
new_string,
|
244
|
+
note_type="long-term note",
|
245
|
+
)
|
246
|
+
self.long_term_note = new_string
|
247
|
+
return result
|
248
|
+
|
249
|
+
def read_contextual_note(
|
250
|
+
self,
|
251
|
+
start_line: int | None = None,
|
252
|
+
end_line: int | None = None,
|
253
|
+
) -> str:
|
254
|
+
"""
|
255
|
+
Read the content of the contextual note, optionally for a specific line range.
|
256
|
+
|
257
|
+
This tool helps you retrieve project-specific or session-specific notes.
|
258
|
+
If the note does not exist, you may want to create it using the write tool.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
start_line (int, optional): 1-based line number to start reading from.
|
262
|
+
end_line (int, optional): 1-based line number to stop reading at (inclusive).
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
str: JSON with file path, content (with line numbers), start/end lines,
|
266
|
+
and total lines.
|
267
|
+
|
268
|
+
Raises:
|
269
|
+
Exception: If the note cannot be read.
|
270
|
+
Suggests writing the note if it does not exist.
|
271
|
+
"""
|
272
|
+
return self._read_note(
|
273
|
+
self._get_contextual_note_path(),
|
274
|
+
start_line,
|
275
|
+
end_line,
|
276
|
+
note_type="contextual note",
|
277
|
+
)
|
278
|
+
|
279
|
+
def write_contextual_note(self, content: str) -> str:
|
280
|
+
"""
|
281
|
+
Write or overwrite the content of the contextual note.
|
282
|
+
|
283
|
+
Use this tool to create a new contextual note or replace its entire content.
|
284
|
+
Always read the note first to avoid accidental data loss, unless you are sure
|
285
|
+
you want to overwrite.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
content (str): The full content to write to the note.
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
str: JSON indicating success and the note path.
|
292
|
+
|
293
|
+
Raises:
|
294
|
+
Exception: If the note cannot be written. Suggests checking permissions or path.
|
295
|
+
"""
|
296
|
+
self.contextual_note = content
|
297
|
+
return self._write_note(
|
298
|
+
self._get_contextual_note_path(), content, note_type="contextual note"
|
299
|
+
)
|
300
|
+
|
301
|
+
def replace_in_contextual_note(
|
302
|
+
self,
|
303
|
+
old_string: str,
|
304
|
+
new_string: str,
|
305
|
+
) -> str:
|
306
|
+
"""
|
307
|
+
Replace the first occurrence of a string in the contextual note.
|
308
|
+
|
309
|
+
Use this tool to update a specific part of the contextual note without
|
310
|
+
overwriting the entire content. If the note does not exist, consider writing it
|
311
|
+
first. If the string is not found, check your input or read the note to verify.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
old_string (str): The exact string to search for and replace.
|
315
|
+
new_string (str): The string to replace with.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
str: JSON indicating success and the note path.
|
319
|
+
|
320
|
+
Raises:
|
321
|
+
Exception: If the note does not exist or the string is not found.
|
322
|
+
Suggests writing or reading the note.
|
323
|
+
"""
|
324
|
+
result = self._replace_in_note(
|
325
|
+
self._get_contextual_note_path(),
|
326
|
+
old_string,
|
327
|
+
new_string,
|
328
|
+
note_type="contextual note",
|
329
|
+
)
|
330
|
+
self.contextual_note = new_string
|
331
|
+
return result
|
332
|
+
|
333
|
+
def _get_long_term_note_path(self) -> str:
|
334
|
+
return os.path.abspath(os.path.expanduser(CFG.LLM_LONG_TERM_NOTE_PATH))
|
335
|
+
|
336
|
+
def _get_contextual_note_path(self) -> str:
|
337
|
+
return os.path.join(self.project_path, CFG.LLM_CONTEXTUAL_NOTE_FILE)
|
338
|
+
|
339
|
+
def _read_note(
|
340
|
+
self,
|
341
|
+
path: str,
|
342
|
+
start_line: int | None = None,
|
343
|
+
end_line: int | None = None,
|
344
|
+
note_type: str = "note",
|
345
|
+
) -> str:
|
346
|
+
"""
|
347
|
+
Internal helper to read a note file with line numbers and error handling.
|
348
|
+
"""
|
349
|
+
if not os.path.exists(path):
|
350
|
+
return json.dumps(
|
351
|
+
{
|
352
|
+
"path": path,
|
353
|
+
"content": "",
|
354
|
+
"start_line": 0,
|
355
|
+
"end_line": 0,
|
356
|
+
"total_lines": 0,
|
357
|
+
}
|
358
|
+
)
|
359
|
+
try:
|
360
|
+
content = read_file_with_line_numbers(path)
|
361
|
+
lines = content.splitlines()
|
362
|
+
total_lines = len(lines)
|
363
|
+
start_idx = (start_line - 1) if start_line is not None else 0
|
364
|
+
end_idx = end_line if end_line is not None else total_lines
|
365
|
+
if start_idx < 0:
|
366
|
+
start_idx = 0
|
367
|
+
if end_idx > total_lines:
|
368
|
+
end_idx = total_lines
|
369
|
+
if start_idx > end_idx:
|
370
|
+
start_idx = end_idx
|
371
|
+
selected_lines = lines[start_idx:end_idx]
|
372
|
+
content_result = "\n".join(selected_lines)
|
373
|
+
return json.dumps(
|
374
|
+
{
|
375
|
+
"path": path,
|
376
|
+
"content": content_result,
|
377
|
+
"start_line": start_idx + 1,
|
378
|
+
"end_line": end_idx,
|
379
|
+
"total_lines": total_lines,
|
380
|
+
}
|
381
|
+
)
|
382
|
+
except Exception:
|
383
|
+
raise Exception(
|
384
|
+
f"Failed to read the {note_type}. "
|
385
|
+
f"If the {note_type} does not exist, try writing it first."
|
386
|
+
)
|
387
|
+
|
388
|
+
def _write_note(self, path: str, content: str, note_type: str = "note") -> str:
|
389
|
+
"""
|
390
|
+
Internal helper to write a note file with error handling.
|
391
|
+
"""
|
392
|
+
try:
|
393
|
+
directory = os.path.dirname(path)
|
394
|
+
if directory and not os.path.exists(directory):
|
395
|
+
os.makedirs(directory, exist_ok=True)
|
396
|
+
write_file(path, content)
|
397
|
+
return json.dumps({"success": True, "path": path})
|
398
|
+
except (OSError, IOError):
|
399
|
+
raise Exception(
|
400
|
+
f"Failed to write the {note_type}. "
|
401
|
+
"Please check if the path is correct and you have write permissions."
|
402
|
+
)
|
403
|
+
except Exception:
|
404
|
+
raise Exception(
|
405
|
+
f"Unexpected error while writing the {note_type}. "
|
406
|
+
"Please check your input and try again."
|
407
|
+
)
|
408
|
+
|
409
|
+
def _replace_in_note(
|
410
|
+
self, path: str, old_string: str, new_string: str, note_type: str = "note"
|
411
|
+
) -> str:
|
412
|
+
"""
|
413
|
+
Internal helper to replace a string in a note file with error handling.
|
414
|
+
"""
|
415
|
+
if not os.path.exists(path):
|
416
|
+
raise Exception(
|
417
|
+
(
|
418
|
+
f"{note_type.capitalize()} not found. "
|
419
|
+
f"Consider writing a new {note_type} first."
|
420
|
+
)
|
421
|
+
)
|
422
|
+
try:
|
423
|
+
content = read_file(path)
|
424
|
+
if old_string not in content:
|
425
|
+
raise Exception(
|
426
|
+
f"The specified string to replace was not found in the {note_type}. ("
|
427
|
+
f"Try reading the {note_type} to verify its content or "
|
428
|
+
f"write a new one if needed)."
|
429
|
+
)
|
430
|
+
new_content = content.replace(old_string, new_string, 1)
|
431
|
+
write_file(path, new_content)
|
432
|
+
return json.dumps({"success": True, "path": path})
|
433
|
+
except Exception:
|
434
|
+
raise Exception(
|
435
|
+
f"Failed to replace content in the {note_type}. ("
|
436
|
+
f"Try reading the {note_type} to verify its content or "
|
437
|
+
"write a new one if needed)."
|
438
|
+
)
|