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.
Files changed (70) hide show
  1. chibi/__init__.py +0 -0
  2. chibi/__main__.py +343 -0
  3. chibi/cli.py +90 -0
  4. chibi/config/__init__.py +6 -0
  5. chibi/config/app.py +123 -0
  6. chibi/config/gpt.py +108 -0
  7. chibi/config/logging.py +15 -0
  8. chibi/config/telegram.py +43 -0
  9. chibi/config_generator.py +233 -0
  10. chibi/constants.py +362 -0
  11. chibi/exceptions.py +58 -0
  12. chibi/models.py +496 -0
  13. chibi/schemas/__init__.py +0 -0
  14. chibi/schemas/anthropic.py +20 -0
  15. chibi/schemas/app.py +54 -0
  16. chibi/schemas/cloudflare.py +65 -0
  17. chibi/schemas/mistralai.py +56 -0
  18. chibi/schemas/suno.py +83 -0
  19. chibi/service.py +135 -0
  20. chibi/services/bot.py +276 -0
  21. chibi/services/lock_manager.py +20 -0
  22. chibi/services/mcp/manager.py +242 -0
  23. chibi/services/metrics.py +54 -0
  24. chibi/services/providers/__init__.py +16 -0
  25. chibi/services/providers/alibaba.py +79 -0
  26. chibi/services/providers/anthropic.py +40 -0
  27. chibi/services/providers/cloudflare.py +98 -0
  28. chibi/services/providers/constants/suno.py +2 -0
  29. chibi/services/providers/customopenai.py +11 -0
  30. chibi/services/providers/deepseek.py +15 -0
  31. chibi/services/providers/eleven_labs.py +85 -0
  32. chibi/services/providers/gemini_native.py +489 -0
  33. chibi/services/providers/grok.py +40 -0
  34. chibi/services/providers/minimax.py +96 -0
  35. chibi/services/providers/mistralai_native.py +312 -0
  36. chibi/services/providers/moonshotai.py +20 -0
  37. chibi/services/providers/openai.py +74 -0
  38. chibi/services/providers/provider.py +892 -0
  39. chibi/services/providers/suno.py +130 -0
  40. chibi/services/providers/tools/__init__.py +23 -0
  41. chibi/services/providers/tools/cmd.py +132 -0
  42. chibi/services/providers/tools/common.py +127 -0
  43. chibi/services/providers/tools/constants.py +78 -0
  44. chibi/services/providers/tools/exceptions.py +1 -0
  45. chibi/services/providers/tools/file_editor.py +875 -0
  46. chibi/services/providers/tools/mcp_management.py +274 -0
  47. chibi/services/providers/tools/mcp_simple.py +72 -0
  48. chibi/services/providers/tools/media.py +451 -0
  49. chibi/services/providers/tools/memory.py +252 -0
  50. chibi/services/providers/tools/schemas.py +10 -0
  51. chibi/services/providers/tools/send.py +435 -0
  52. chibi/services/providers/tools/tool.py +163 -0
  53. chibi/services/providers/tools/utils.py +146 -0
  54. chibi/services/providers/tools/web.py +261 -0
  55. chibi/services/providers/utils.py +182 -0
  56. chibi/services/task_manager.py +93 -0
  57. chibi/services/user.py +269 -0
  58. chibi/storage/abstract.py +54 -0
  59. chibi/storage/database.py +86 -0
  60. chibi/storage/dynamodb.py +257 -0
  61. chibi/storage/local.py +70 -0
  62. chibi/storage/redis.py +91 -0
  63. chibi/utils/__init__.py +0 -0
  64. chibi/utils/app.py +249 -0
  65. chibi/utils/telegram.py +521 -0
  66. chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
  67. chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
  68. chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
  69. chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
  70. 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}")