chibi-bot 1.6.0b0__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.
- chibi/__init__.py +0 -0
- chibi/__main__.py +343 -0
- chibi/cli.py +90 -0
- chibi/config/__init__.py +6 -0
- chibi/config/app.py +123 -0
- chibi/config/gpt.py +108 -0
- chibi/config/logging.py +15 -0
- chibi/config/telegram.py +43 -0
- chibi/config_generator.py +233 -0
- chibi/constants.py +362 -0
- chibi/exceptions.py +58 -0
- chibi/models.py +496 -0
- chibi/schemas/__init__.py +0 -0
- chibi/schemas/anthropic.py +20 -0
- chibi/schemas/app.py +54 -0
- chibi/schemas/cloudflare.py +65 -0
- chibi/schemas/mistralai.py +56 -0
- chibi/schemas/suno.py +83 -0
- chibi/service.py +135 -0
- chibi/services/bot.py +276 -0
- chibi/services/lock_manager.py +20 -0
- chibi/services/mcp/manager.py +242 -0
- chibi/services/metrics.py +54 -0
- chibi/services/providers/__init__.py +16 -0
- chibi/services/providers/alibaba.py +79 -0
- chibi/services/providers/anthropic.py +40 -0
- chibi/services/providers/cloudflare.py +98 -0
- chibi/services/providers/constants/suno.py +2 -0
- chibi/services/providers/customopenai.py +11 -0
- chibi/services/providers/deepseek.py +15 -0
- chibi/services/providers/eleven_labs.py +85 -0
- chibi/services/providers/gemini_native.py +489 -0
- chibi/services/providers/grok.py +40 -0
- chibi/services/providers/minimax.py +96 -0
- chibi/services/providers/mistralai_native.py +312 -0
- chibi/services/providers/moonshotai.py +20 -0
- chibi/services/providers/openai.py +74 -0
- chibi/services/providers/provider.py +892 -0
- chibi/services/providers/suno.py +130 -0
- chibi/services/providers/tools/__init__.py +23 -0
- chibi/services/providers/tools/cmd.py +132 -0
- chibi/services/providers/tools/common.py +127 -0
- chibi/services/providers/tools/constants.py +78 -0
- chibi/services/providers/tools/exceptions.py +1 -0
- chibi/services/providers/tools/file_editor.py +875 -0
- chibi/services/providers/tools/mcp_management.py +274 -0
- chibi/services/providers/tools/mcp_simple.py +72 -0
- chibi/services/providers/tools/media.py +451 -0
- chibi/services/providers/tools/memory.py +252 -0
- chibi/services/providers/tools/schemas.py +10 -0
- chibi/services/providers/tools/send.py +435 -0
- chibi/services/providers/tools/tool.py +163 -0
- chibi/services/providers/tools/utils.py +146 -0
- chibi/services/providers/tools/web.py +261 -0
- chibi/services/providers/utils.py +182 -0
- chibi/services/task_manager.py +93 -0
- chibi/services/user.py +269 -0
- chibi/storage/abstract.py +54 -0
- chibi/storage/database.py +86 -0
- chibi/storage/dynamodb.py +257 -0
- chibi/storage/local.py +70 -0
- chibi/storage/redis.py +91 -0
- chibi/utils/__init__.py +0 -0
- chibi/utils/app.py +249 -0
- chibi/utils/telegram.py +521 -0
- chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
- chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
- chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
- chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
- chibi_bot-1.6.0b0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File editing utilities for the project.
|
|
3
|
+
This module provides functions for efficiently editing files through LLM tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Unpack
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from openai.types.chat import ChatCompletionToolParam
|
|
13
|
+
from openai.types.shared_params import FunctionDefinition
|
|
14
|
+
|
|
15
|
+
from chibi.config import gpt_settings
|
|
16
|
+
from chibi.services.providers.tools.exceptions import ToolException
|
|
17
|
+
from chibi.services.providers.tools.tool import ChibiTool
|
|
18
|
+
from chibi.services.providers.tools.utils import AdditionalOptions
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReplaceInFileTool(ChibiTool):
|
|
22
|
+
register = gpt_settings.filesystem_access
|
|
23
|
+
definition = ChatCompletionToolParam(
|
|
24
|
+
type="function",
|
|
25
|
+
function=FunctionDefinition(
|
|
26
|
+
name="replace_in_file",
|
|
27
|
+
description="Replace occurrences of text in a file.",
|
|
28
|
+
parameters={
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"full_path": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Path to the file.",
|
|
34
|
+
},
|
|
35
|
+
"old_text": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Text to be replaced.",
|
|
38
|
+
},
|
|
39
|
+
"new_text": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Replacement text.",
|
|
42
|
+
},
|
|
43
|
+
"count": {
|
|
44
|
+
"type": "integer",
|
|
45
|
+
"description": "Maximum number of replacements to make (-1 for all).",
|
|
46
|
+
},
|
|
47
|
+
"encoding": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "File encoding.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"required": ["full_path", "old_text", "new_text"],
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
name = "replace_in_file"
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
async def function(
|
|
60
|
+
cls,
|
|
61
|
+
full_path: str,
|
|
62
|
+
old_text: str,
|
|
63
|
+
new_text: str,
|
|
64
|
+
count: int = -1,
|
|
65
|
+
encoding: str = "utf-8",
|
|
66
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
67
|
+
) -> dict[str, int]:
|
|
68
|
+
"""
|
|
69
|
+
Replace occurrences of text in a file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
full_path: Path to the file
|
|
73
|
+
old_text: Text to be replaced
|
|
74
|
+
new_text: Replacement text
|
|
75
|
+
count: Maximum number of replacements to make (-1 for all)
|
|
76
|
+
encoding: File encoding
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dict containing the number of replacements made
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
path = Path(full_path).expanduser().resolve()
|
|
83
|
+
if not path.exists():
|
|
84
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
85
|
+
|
|
86
|
+
with path.open("r", encoding=encoding) as f:
|
|
87
|
+
content = f.read()
|
|
88
|
+
|
|
89
|
+
new_content, replacements = content, 0
|
|
90
|
+
if count == -1:
|
|
91
|
+
new_content, replacements = content.replace(old_text, new_text), content.count(old_text)
|
|
92
|
+
else:
|
|
93
|
+
new_content, replacements = re.subn(
|
|
94
|
+
re.escape(old_text), new_text.replace("\\", "\\\\"), content, count=count
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if replacements > 0:
|
|
98
|
+
with path.open("w", encoding=encoding) as f:
|
|
99
|
+
f.write(new_content)
|
|
100
|
+
logger.log(
|
|
101
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] Made {replacements} replacements in {path}"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
logger.log(
|
|
105
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] No occurrences of '{old_text}' found in {path}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return {"replacements": replacements}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error replacing text in {full_path}: {e}")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ReplaceInFileRegexTool(ChibiTool):
|
|
115
|
+
register = gpt_settings.filesystem_access
|
|
116
|
+
definition = ChatCompletionToolParam(
|
|
117
|
+
type="function",
|
|
118
|
+
function=FunctionDefinition(
|
|
119
|
+
name="replace_in_file_regex",
|
|
120
|
+
description="Replace text in a file using regex patterns.",
|
|
121
|
+
parameters={
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"full_path": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Path to the file.",
|
|
127
|
+
},
|
|
128
|
+
"pattern": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "Regex pattern to match.",
|
|
131
|
+
},
|
|
132
|
+
"replacement": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Replacement text (can include regex group references).",
|
|
135
|
+
},
|
|
136
|
+
"count": {
|
|
137
|
+
"type": "integer",
|
|
138
|
+
"description": "Maximum number of replacements to make (-1 for all).",
|
|
139
|
+
},
|
|
140
|
+
"encoding": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "File encoding.",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"required": ["full_path", "pattern", "replacement"],
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
name = "replace_in_file_regex"
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
async def function(
|
|
153
|
+
cls,
|
|
154
|
+
full_path: str,
|
|
155
|
+
pattern: str,
|
|
156
|
+
replacement: str,
|
|
157
|
+
count: int = -1,
|
|
158
|
+
encoding: str = "utf-8",
|
|
159
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
160
|
+
) -> dict[str, int]:
|
|
161
|
+
"""Replace text in a file using regex patterns.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
full_path: Path to the file
|
|
165
|
+
pattern: Regex pattern to match
|
|
166
|
+
replacement: Replacement text (can include regex group references)
|
|
167
|
+
count: Maximum number of replacements to make (-1 for all)
|
|
168
|
+
encoding: File encoding
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Dict containing the number of replacements made
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
path = Path(full_path).expanduser().resolve()
|
|
175
|
+
if not path.exists():
|
|
176
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
177
|
+
|
|
178
|
+
with path.open("r", encoding=encoding) as f:
|
|
179
|
+
content = f.read()
|
|
180
|
+
|
|
181
|
+
new_content, replacements = re.subn(pattern, replacement, content, count=count)
|
|
182
|
+
|
|
183
|
+
if replacements > 0:
|
|
184
|
+
with path.open("w", encoding=encoding) as f:
|
|
185
|
+
f.write(new_content)
|
|
186
|
+
logger.log(
|
|
187
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] Made {replacements} regex replacements in {path}"
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
logger.log(
|
|
191
|
+
"TOOL",
|
|
192
|
+
f"[{kwargs.get('model', 'Unknown model')}] No matches for pattern '{pattern}' found in {path}",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return {"replacements": replacements}
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(
|
|
198
|
+
f"[{kwargs.get('model', 'Unknown model')}] Error replacing text with regex in {full_path}: {e}"
|
|
199
|
+
)
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class InsertAtLineTool(ChibiTool):
|
|
204
|
+
register = gpt_settings.filesystem_access
|
|
205
|
+
definition = ChatCompletionToolParam(
|
|
206
|
+
type="function",
|
|
207
|
+
function=FunctionDefinition(
|
|
208
|
+
name="insert_at_line",
|
|
209
|
+
description="Insert content at a specific line number in a file.",
|
|
210
|
+
parameters={
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"full_path": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"description": "Path to the file.",
|
|
216
|
+
},
|
|
217
|
+
"line_number": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"description": "Line number where to insert (0-indexed).",
|
|
220
|
+
},
|
|
221
|
+
"content": {
|
|
222
|
+
"type": "string",
|
|
223
|
+
"description": "Content to insert.",
|
|
224
|
+
},
|
|
225
|
+
"encoding": {
|
|
226
|
+
"type": "string",
|
|
227
|
+
"description": "File encoding.",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
"required": ["full_path", "line_number", "content"],
|
|
231
|
+
},
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
name = "insert_at_line"
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
async def function(
|
|
238
|
+
cls,
|
|
239
|
+
full_path: str,
|
|
240
|
+
line_number: int,
|
|
241
|
+
content: str,
|
|
242
|
+
encoding: str = "utf-8",
|
|
243
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
244
|
+
) -> dict[str, bool]:
|
|
245
|
+
"""Insert content at a specific line number in a file.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
full_path: Path to the file
|
|
249
|
+
line_number: Line number where to insert (0-indexed)
|
|
250
|
+
content: Content to insert
|
|
251
|
+
encoding: File encoding
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict indicating success
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
path = Path(full_path).expanduser().resolve()
|
|
258
|
+
if not path.exists():
|
|
259
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
260
|
+
|
|
261
|
+
with path.open("r", encoding=encoding) as f:
|
|
262
|
+
lines = f.readlines()
|
|
263
|
+
|
|
264
|
+
line_count = len(lines)
|
|
265
|
+
if line_number < 0:
|
|
266
|
+
line_number = max(0, line_count + line_number)
|
|
267
|
+
elif line_number > line_count:
|
|
268
|
+
line_number = line_count
|
|
269
|
+
|
|
270
|
+
if content and not content.endswith("\n"):
|
|
271
|
+
content += "\n"
|
|
272
|
+
|
|
273
|
+
lines.insert(line_number, content)
|
|
274
|
+
|
|
275
|
+
with path.open("w", encoding=encoding) as f:
|
|
276
|
+
f.writelines(lines)
|
|
277
|
+
|
|
278
|
+
logger.log(
|
|
279
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] Inserted content at line {line_number} in {path}"
|
|
280
|
+
)
|
|
281
|
+
return {"success": True}
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(
|
|
285
|
+
f"[{kwargs.get('model', 'Unknown model')}] Error inserting at line {line_number} in {full_path}: {e}"
|
|
286
|
+
)
|
|
287
|
+
raise
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class ReplaceLinesTool(ChibiTool):
|
|
291
|
+
register = gpt_settings.filesystem_access
|
|
292
|
+
definition = ChatCompletionToolParam(
|
|
293
|
+
type="function",
|
|
294
|
+
function=FunctionDefinition(
|
|
295
|
+
name="replace_lines",
|
|
296
|
+
description="Replace a range of lines in a file with new content.",
|
|
297
|
+
parameters={
|
|
298
|
+
"type": "object",
|
|
299
|
+
"properties": {
|
|
300
|
+
"full_path": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Path to the file.",
|
|
303
|
+
},
|
|
304
|
+
"start_line": {
|
|
305
|
+
"type": "integer",
|
|
306
|
+
"description": "First line to replace (0-indexed).",
|
|
307
|
+
},
|
|
308
|
+
"end_line": {
|
|
309
|
+
"type": "integer",
|
|
310
|
+
"description": "Last line to replace (inclusive).",
|
|
311
|
+
},
|
|
312
|
+
"new_content": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"description": "New content to insert.",
|
|
315
|
+
},
|
|
316
|
+
"encoding": {
|
|
317
|
+
"type": "string",
|
|
318
|
+
"description": "File encoding.",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
"required": ["full_path", "start_line", "end_line", "new_content"],
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
name = "replace_lines"
|
|
326
|
+
|
|
327
|
+
@classmethod
|
|
328
|
+
async def function(
|
|
329
|
+
cls,
|
|
330
|
+
full_path: str,
|
|
331
|
+
start_line: int,
|
|
332
|
+
end_line: int,
|
|
333
|
+
new_content: str,
|
|
334
|
+
encoding: str = "utf-8",
|
|
335
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
336
|
+
) -> dict[str, int]:
|
|
337
|
+
"""Replace a range of lines in a file with new content.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
full_path: Path to the file
|
|
341
|
+
start_line: First line to replace (0-indexed)
|
|
342
|
+
end_line: Last line to replace (inclusive)
|
|
343
|
+
new_content: New content to insert
|
|
344
|
+
encoding: File encoding
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Dict containing the number of lines replaced
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
path = Path(full_path).expanduser().resolve()
|
|
351
|
+
if not path.exists():
|
|
352
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
353
|
+
|
|
354
|
+
with path.open("r", encoding=encoding) as f:
|
|
355
|
+
lines = f.readlines()
|
|
356
|
+
|
|
357
|
+
line_count = len(lines)
|
|
358
|
+
|
|
359
|
+
if start_line < 0:
|
|
360
|
+
start_line = max(0, line_count + start_line)
|
|
361
|
+
if end_line < 0:
|
|
362
|
+
end_line = max(0, line_count + end_line)
|
|
363
|
+
|
|
364
|
+
start_line = min(max(0, start_line), line_count)
|
|
365
|
+
end_line = min(max(start_line, end_line), line_count - 1)
|
|
366
|
+
|
|
367
|
+
if new_content and not new_content.endswith("\n"):
|
|
368
|
+
new_content += "\n"
|
|
369
|
+
new_lines = new_content.splitlines(True)
|
|
370
|
+
|
|
371
|
+
lines[start_line : end_line + 1] = new_lines
|
|
372
|
+
lines_replaced = end_line - start_line + 1
|
|
373
|
+
|
|
374
|
+
with path.open("w", encoding=encoding) as f:
|
|
375
|
+
f.writelines(lines)
|
|
376
|
+
|
|
377
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Replaced {lines_replaced} lines in {path}")
|
|
378
|
+
return {"lines_replaced": lines_replaced}
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error replacing lines in {full_path}: {e}")
|
|
382
|
+
raise
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class FindAndReplaceSectionTool(ChibiTool):
|
|
386
|
+
register = gpt_settings.filesystem_access
|
|
387
|
+
definition = ChatCompletionToolParam(
|
|
388
|
+
type="function",
|
|
389
|
+
function=FunctionDefinition(
|
|
390
|
+
name="find_and_replace_section",
|
|
391
|
+
description="Find and replace a marked section in a file.",
|
|
392
|
+
parameters={
|
|
393
|
+
"type": "object",
|
|
394
|
+
"properties": {
|
|
395
|
+
"full_path": {
|
|
396
|
+
"type": "string",
|
|
397
|
+
"description": "Path to the file.",
|
|
398
|
+
},
|
|
399
|
+
"start_marker": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"description": "Text that marks the beginning of the section.",
|
|
402
|
+
},
|
|
403
|
+
"end_marker": {
|
|
404
|
+
"type": "string",
|
|
405
|
+
"description": "Text that marks the end of the section.",
|
|
406
|
+
},
|
|
407
|
+
"new_content": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"description": "New content to replace the section with.",
|
|
410
|
+
},
|
|
411
|
+
"include_markers": {
|
|
412
|
+
"type": "boolean",
|
|
413
|
+
"description": "Whether to include the markers in the replacement.",
|
|
414
|
+
},
|
|
415
|
+
"encoding": {
|
|
416
|
+
"type": "string",
|
|
417
|
+
"description": "File encoding.",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
"required": ["full_path", "start_marker", "end_marker", "new_content"],
|
|
421
|
+
},
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
name = "find_and_replace_section"
|
|
425
|
+
|
|
426
|
+
@classmethod
|
|
427
|
+
async def function(
|
|
428
|
+
cls,
|
|
429
|
+
full_path: str,
|
|
430
|
+
start_marker: str,
|
|
431
|
+
end_marker: str,
|
|
432
|
+
new_content: str,
|
|
433
|
+
include_markers: bool = True,
|
|
434
|
+
encoding: str = "utf-8",
|
|
435
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
436
|
+
) -> dict[str, bool]:
|
|
437
|
+
"""Find and replace a marked section in a file.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
full_path: Path to the file
|
|
441
|
+
start_marker: Text that marks the beginning of the section
|
|
442
|
+
end_marker: Text that marks the end of the section
|
|
443
|
+
new_content: New content to replace the section with
|
|
444
|
+
include_markers: Whether to include the markers in the replacement
|
|
445
|
+
encoding: File encoding
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Dict indicating success and whether the section was found
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
path = Path(full_path).expanduser().resolve()
|
|
452
|
+
if not path.exists():
|
|
453
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
454
|
+
|
|
455
|
+
with path.open("r", encoding=encoding) as f:
|
|
456
|
+
content = f.read()
|
|
457
|
+
|
|
458
|
+
if start_marker not in content or end_marker not in content:
|
|
459
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Section markers not found in {path}")
|
|
460
|
+
return {"success": False, "section_found": False}
|
|
461
|
+
|
|
462
|
+
start_pos = content.find(start_marker)
|
|
463
|
+
end_pos = content.find(end_marker, start_pos + len(start_marker))
|
|
464
|
+
|
|
465
|
+
if start_pos == -1 or end_pos == -1:
|
|
466
|
+
logger.log(
|
|
467
|
+
"TOOL",
|
|
468
|
+
f"[{kwargs.get('model', 'Unknown model')}] Section markers not found in correct order in {path}",
|
|
469
|
+
)
|
|
470
|
+
return {"success": False, "section_found": False}
|
|
471
|
+
|
|
472
|
+
if include_markers:
|
|
473
|
+
end_pos += len(end_marker)
|
|
474
|
+
body = new_content
|
|
475
|
+
|
|
476
|
+
if body.strip().startswith(start_marker.strip()):
|
|
477
|
+
body = body.strip()[len(start_marker.strip()) :].lstrip("\n")
|
|
478
|
+
|
|
479
|
+
if body.strip().endswith(end_marker.strip()):
|
|
480
|
+
body = body.strip()[: -len(end_marker.strip())].rstrip("\n")
|
|
481
|
+
|
|
482
|
+
# Check if markers are on the same line (no newline between them)
|
|
483
|
+
section_content = content[start_pos : end_pos + len(end_marker)]
|
|
484
|
+
if "\n" not in section_content:
|
|
485
|
+
# Single line case - no newlines around content
|
|
486
|
+
replacement = f"{start_marker}{body}{end_marker}"
|
|
487
|
+
else:
|
|
488
|
+
# Multi-line case - preserve newlines
|
|
489
|
+
replacement = f"{start_marker}\n{body}\n{end_marker}"
|
|
490
|
+
else:
|
|
491
|
+
end_pos += len(end_marker)
|
|
492
|
+
replacement = new_content
|
|
493
|
+
|
|
494
|
+
new_content = content[:start_pos] + replacement + content[end_pos:]
|
|
495
|
+
|
|
496
|
+
with path.open("w", encoding=encoding) as f:
|
|
497
|
+
f.write(new_content)
|
|
498
|
+
|
|
499
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Replaced section in {path}")
|
|
500
|
+
return {"success": True, "section_found": True}
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error replacing section in {full_path}: {e}")
|
|
504
|
+
raise
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class AppendToFileTool(ChibiTool):
|
|
508
|
+
register = gpt_settings.filesystem_access
|
|
509
|
+
definition = ChatCompletionToolParam(
|
|
510
|
+
type="function",
|
|
511
|
+
function=FunctionDefinition(
|
|
512
|
+
name="append_to_file",
|
|
513
|
+
description="Append content to the end of a file.",
|
|
514
|
+
parameters={
|
|
515
|
+
"type": "object",
|
|
516
|
+
"properties": {
|
|
517
|
+
"full_path": {
|
|
518
|
+
"type": "string",
|
|
519
|
+
"description": "Path to the file.",
|
|
520
|
+
},
|
|
521
|
+
"content": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"description": "Content to append.",
|
|
524
|
+
},
|
|
525
|
+
"ensure_newline": {
|
|
526
|
+
"type": "boolean",
|
|
527
|
+
"description": "Ensure file ends with a newline before appending.",
|
|
528
|
+
},
|
|
529
|
+
"encoding": {
|
|
530
|
+
"type": "string",
|
|
531
|
+
"description": "File encoding.",
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
"required": ["full_path", "content"],
|
|
535
|
+
},
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
name = "append_to_file"
|
|
539
|
+
|
|
540
|
+
@classmethod
|
|
541
|
+
async def function(
|
|
542
|
+
cls,
|
|
543
|
+
full_path: str,
|
|
544
|
+
content: str,
|
|
545
|
+
ensure_newline: bool = True,
|
|
546
|
+
encoding: str = "utf-8",
|
|
547
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
548
|
+
) -> dict[str, bool]:
|
|
549
|
+
"""Append content to the end of a file.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
full_path: Path to the file
|
|
553
|
+
content: Content to append
|
|
554
|
+
ensure_newline: Ensure file ends with a newline before appending
|
|
555
|
+
encoding: File encoding
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Dict indicating success
|
|
559
|
+
"""
|
|
560
|
+
try:
|
|
561
|
+
path = Path(full_path).expanduser().resolve()
|
|
562
|
+
if not path.exists():
|
|
563
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
564
|
+
|
|
565
|
+
needs_newline = False
|
|
566
|
+
if ensure_newline and os.path.getsize(path) > 0:
|
|
567
|
+
with path.open("rb") as f:
|
|
568
|
+
f.seek(-1, os.SEEK_END)
|
|
569
|
+
last_char = f.read(1)
|
|
570
|
+
needs_newline = last_char != b"\n"
|
|
571
|
+
|
|
572
|
+
with path.open("a", encoding=encoding) as f:
|
|
573
|
+
if needs_newline:
|
|
574
|
+
f.write("\n")
|
|
575
|
+
f.write(content)
|
|
576
|
+
|
|
577
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Appended content to {path}")
|
|
578
|
+
return {"success": True}
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error appending to {full_path}: {e}")
|
|
582
|
+
raise
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class InsertAfterPatternTool(ChibiTool):
|
|
586
|
+
register = gpt_settings.filesystem_access
|
|
587
|
+
definition = ChatCompletionToolParam(
|
|
588
|
+
type="function",
|
|
589
|
+
function=FunctionDefinition(
|
|
590
|
+
name="insert_after_pattern",
|
|
591
|
+
description="Insert content after a specific pattern (string or regex) in the file.",
|
|
592
|
+
parameters={
|
|
593
|
+
"type": "object",
|
|
594
|
+
"properties": {
|
|
595
|
+
"full_path": {
|
|
596
|
+
"type": "string",
|
|
597
|
+
"description": "Path to the file.",
|
|
598
|
+
},
|
|
599
|
+
"pattern": {
|
|
600
|
+
"type": "string",
|
|
601
|
+
"description": "Pattern to search for.",
|
|
602
|
+
},
|
|
603
|
+
"content": {
|
|
604
|
+
"type": "string",
|
|
605
|
+
"description": "Content to insert after the pattern.",
|
|
606
|
+
},
|
|
607
|
+
"regex": {
|
|
608
|
+
"type": "boolean",
|
|
609
|
+
"description": "Whether to treat pattern as regex.",
|
|
610
|
+
},
|
|
611
|
+
"count": {
|
|
612
|
+
"type": "integer",
|
|
613
|
+
"description": "Maximum number of insertions to make.",
|
|
614
|
+
},
|
|
615
|
+
"encoding": {
|
|
616
|
+
"type": "string",
|
|
617
|
+
"description": "File encoding.",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
"required": ["full_path", "pattern", "content"],
|
|
621
|
+
},
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
name = "insert_after_pattern"
|
|
625
|
+
|
|
626
|
+
@classmethod
|
|
627
|
+
async def function(
|
|
628
|
+
cls,
|
|
629
|
+
full_path: str,
|
|
630
|
+
pattern: str,
|
|
631
|
+
content: str,
|
|
632
|
+
regex: bool = False,
|
|
633
|
+
count: int = 1,
|
|
634
|
+
encoding: str = "utf-8",
|
|
635
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
636
|
+
) -> dict[str, int]:
|
|
637
|
+
"""Insert content after a specific pattern (string or regex) in the file.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
full_path: Path to the file
|
|
641
|
+
pattern: Pattern to search for
|
|
642
|
+
content: Content to insert after the pattern
|
|
643
|
+
regex: Whether to treat pattern as regex
|
|
644
|
+
count: Maximum number of insertions to make
|
|
645
|
+
encoding: File encoding
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Dict containing the number of insertions made
|
|
649
|
+
"""
|
|
650
|
+
try:
|
|
651
|
+
path = Path(full_path).expanduser().resolve()
|
|
652
|
+
if not path.exists():
|
|
653
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
654
|
+
|
|
655
|
+
with path.open("r", encoding=encoding) as f:
|
|
656
|
+
file_content = f.read()
|
|
657
|
+
|
|
658
|
+
if regex:
|
|
659
|
+
matches = list(re.finditer(pattern, file_content))
|
|
660
|
+
if not matches:
|
|
661
|
+
logger.log(
|
|
662
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] Pattern '{pattern}' not found in {path}"
|
|
663
|
+
)
|
|
664
|
+
return {"insertions": 0}
|
|
665
|
+
|
|
666
|
+
matches = matches[:count] # Limit to the specified count
|
|
667
|
+
|
|
668
|
+
new_content = file_content
|
|
669
|
+
for match in reversed(matches):
|
|
670
|
+
pos = match.end()
|
|
671
|
+
new_content = new_content[:pos] + content + new_content[pos:]
|
|
672
|
+
|
|
673
|
+
insertions = len(matches)
|
|
674
|
+
else:
|
|
675
|
+
insertions = 0
|
|
676
|
+
current_pos = 0
|
|
677
|
+
new_content = file_content
|
|
678
|
+
|
|
679
|
+
for _ in range(count):
|
|
680
|
+
pos = new_content.find(pattern, current_pos)
|
|
681
|
+
if pos == -1:
|
|
682
|
+
break
|
|
683
|
+
|
|
684
|
+
insert_pos = pos + len(pattern)
|
|
685
|
+
new_content = new_content[:insert_pos] + content + new_content[insert_pos:]
|
|
686
|
+
current_pos = insert_pos + len(content)
|
|
687
|
+
insertions += 1
|
|
688
|
+
|
|
689
|
+
if insertions > 0:
|
|
690
|
+
with path.open("w", encoding=encoding) as f:
|
|
691
|
+
f.write(new_content)
|
|
692
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Made {insertions} insertions in {path}")
|
|
693
|
+
|
|
694
|
+
return {"insertions": insertions}
|
|
695
|
+
|
|
696
|
+
except Exception as e:
|
|
697
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error inserting after pattern in {full_path}: {e}")
|
|
698
|
+
raise
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class InsertBeforePatternTool(ChibiTool):
|
|
702
|
+
register = gpt_settings.filesystem_access
|
|
703
|
+
definition = ChatCompletionToolParam(
|
|
704
|
+
type="function",
|
|
705
|
+
function=FunctionDefinition(
|
|
706
|
+
name="insert_before_pattern",
|
|
707
|
+
description="Insert content before a specific pattern (string or regex) in the file.",
|
|
708
|
+
parameters={
|
|
709
|
+
"type": "object",
|
|
710
|
+
"properties": {
|
|
711
|
+
"full_path": {
|
|
712
|
+
"type": "string",
|
|
713
|
+
"description": "Path to the file.",
|
|
714
|
+
},
|
|
715
|
+
"pattern": {
|
|
716
|
+
"type": "string",
|
|
717
|
+
"description": "Pattern to search for.",
|
|
718
|
+
},
|
|
719
|
+
"content": {
|
|
720
|
+
"type": "string",
|
|
721
|
+
"description": "Content to insert before the pattern.",
|
|
722
|
+
},
|
|
723
|
+
"regex": {
|
|
724
|
+
"type": "boolean",
|
|
725
|
+
"description": "Whether to treat pattern as regex.",
|
|
726
|
+
},
|
|
727
|
+
"count": {
|
|
728
|
+
"type": "integer",
|
|
729
|
+
"description": "Maximum number of insertions to make.",
|
|
730
|
+
},
|
|
731
|
+
"encoding": {
|
|
732
|
+
"type": "string",
|
|
733
|
+
"description": "File encoding.",
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
"required": ["full_path", "pattern", "content"],
|
|
737
|
+
},
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
name = "insert_before_pattern"
|
|
741
|
+
|
|
742
|
+
@classmethod
|
|
743
|
+
async def function(
|
|
744
|
+
cls,
|
|
745
|
+
full_path: str,
|
|
746
|
+
pattern: str,
|
|
747
|
+
content: str,
|
|
748
|
+
regex: bool = False,
|
|
749
|
+
count: int = 1,
|
|
750
|
+
encoding: str = "utf-8",
|
|
751
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
752
|
+
) -> dict[str, int]:
|
|
753
|
+
"""Insert content before a specific pattern (string or regex) in the file.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
full_path: Path to the file
|
|
757
|
+
pattern: Pattern to search for
|
|
758
|
+
content: Content to insert before the pattern
|
|
759
|
+
regex: Whether to treat pattern as regex
|
|
760
|
+
count: Maximum number of insertions to make
|
|
761
|
+
encoding: File encoding
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Dict containing the number of insertions made
|
|
765
|
+
"""
|
|
766
|
+
try:
|
|
767
|
+
path = Path(full_path).expanduser().resolve()
|
|
768
|
+
if not path.exists():
|
|
769
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
770
|
+
|
|
771
|
+
with path.open("r", encoding=encoding) as f:
|
|
772
|
+
file_content = f.read()
|
|
773
|
+
|
|
774
|
+
if regex:
|
|
775
|
+
matches = list(re.finditer(pattern, file_content))
|
|
776
|
+
if not matches:
|
|
777
|
+
logger.log(
|
|
778
|
+
"TOOL", f"[{kwargs.get('model', 'Unknown model')}] Pattern '{pattern}' not found in {path}"
|
|
779
|
+
)
|
|
780
|
+
return {"insertions": 0}
|
|
781
|
+
|
|
782
|
+
matches = matches[:count] # Limit to the specified count
|
|
783
|
+
|
|
784
|
+
new_content = file_content
|
|
785
|
+
for match in reversed(matches):
|
|
786
|
+
pos = match.start()
|
|
787
|
+
new_content = new_content[:pos] + content + new_content[pos:]
|
|
788
|
+
|
|
789
|
+
insertions = len(matches)
|
|
790
|
+
else:
|
|
791
|
+
insertions = 0
|
|
792
|
+
current_pos = 0
|
|
793
|
+
new_content = file_content
|
|
794
|
+
|
|
795
|
+
for _ in range(count):
|
|
796
|
+
pos = new_content.find(pattern, current_pos)
|
|
797
|
+
if pos == -1:
|
|
798
|
+
break
|
|
799
|
+
|
|
800
|
+
new_content = new_content[:pos] + content + new_content[pos:]
|
|
801
|
+
current_pos = pos + len(content) + len(pattern)
|
|
802
|
+
insertions += 1
|
|
803
|
+
|
|
804
|
+
if insertions > 0:
|
|
805
|
+
with path.open("w", encoding=encoding) as f:
|
|
806
|
+
f.write(new_content)
|
|
807
|
+
logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Made {insertions} insertions in {path}")
|
|
808
|
+
|
|
809
|
+
return {"insertions": insertions}
|
|
810
|
+
|
|
811
|
+
except Exception as e:
|
|
812
|
+
logger.error(f"[{kwargs.get('model', 'Unknown model')}] Error inserting before pattern in {full_path}: {e}")
|
|
813
|
+
raise
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
class CreateFileTool(ChibiTool):
|
|
817
|
+
register = gpt_settings.filesystem_access
|
|
818
|
+
definition = ChatCompletionToolParam(
|
|
819
|
+
type="function",
|
|
820
|
+
function=FunctionDefinition(
|
|
821
|
+
name="create_file",
|
|
822
|
+
description="Create a file at the given full path (including any directories).",
|
|
823
|
+
parameters={
|
|
824
|
+
"type": "object",
|
|
825
|
+
"properties": {
|
|
826
|
+
"content": {
|
|
827
|
+
"type": "string",
|
|
828
|
+
"description": "File content",
|
|
829
|
+
},
|
|
830
|
+
"full_path": {
|
|
831
|
+
"type": "string",
|
|
832
|
+
"description": "Full file name, including file path.",
|
|
833
|
+
},
|
|
834
|
+
"overwrite": {
|
|
835
|
+
"type": "string",
|
|
836
|
+
"enum": ["true", "false"],
|
|
837
|
+
"description": (
|
|
838
|
+
"Set it to true to overwrite the file. If overwrite is false and "
|
|
839
|
+
"the file exists, raises FileExistsError."
|
|
840
|
+
),
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
"required": ["content", "full_path", "overwrite"],
|
|
844
|
+
},
|
|
845
|
+
),
|
|
846
|
+
)
|
|
847
|
+
name = "create_file"
|
|
848
|
+
|
|
849
|
+
@classmethod
|
|
850
|
+
async def function(
|
|
851
|
+
cls,
|
|
852
|
+
full_path: str,
|
|
853
|
+
content: str,
|
|
854
|
+
overwrite: str = "false",
|
|
855
|
+
encoding: str = "utf-8",
|
|
856
|
+
**kwargs: Unpack[AdditionalOptions],
|
|
857
|
+
) -> dict[str, Any]:
|
|
858
|
+
try:
|
|
859
|
+
path = Path(full_path).expanduser().resolve()
|
|
860
|
+
parent = path.parent
|
|
861
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
862
|
+
|
|
863
|
+
if path.exists():
|
|
864
|
+
if overwrite != "true":
|
|
865
|
+
raise FileExistsError(f"File {path} already exists")
|
|
866
|
+
logger.log("TOOL", f"File {path} exists. Overwriting.")
|
|
867
|
+
|
|
868
|
+
with path.open("w", encoding=encoding) as f:
|
|
869
|
+
f.write(content)
|
|
870
|
+
|
|
871
|
+
logger.log("TOOL", f"File {path} created successfully.")
|
|
872
|
+
return {"file": str(path)}
|
|
873
|
+
|
|
874
|
+
except Exception as e:
|
|
875
|
+
raise ToolException(f"Failed to create file {full_path}. Error: {e}")
|