code-puppy 0.0.124__py3-none-any.whl → 0.0.126__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.
- code_puppy/agent.py +20 -4
- code_puppy/agents/__init__.py +25 -0
- code_puppy/{agent_prompts.py → agents/agent_code_puppy.py} +46 -13
- code_puppy/agents/agent_creator_agent.py +446 -0
- code_puppy/agents/agent_manager.py +211 -0
- code_puppy/agents/base_agent.py +60 -0
- code_puppy/agents/json_agent.py +129 -0
- code_puppy/callbacks.py +6 -0
- code_puppy/command_line/command_handler.py +91 -1
- code_puppy/config.py +13 -1
- code_puppy/main.py +6 -1
- code_puppy/tools/__init__.py +60 -7
- code_puppy/tools/command_runner.py +97 -0
- code_puppy/tools/file_modifications.py +176 -11
- code_puppy/tools/file_operations.py +171 -1
- code_puppy/tui/app.py +14 -158
- code_puppy/tui/tests/test_agent_command.py +72 -0
- code_puppy-0.0.126.dist-info/METADATA +634 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/RECORD +23 -17
- code_puppy-0.0.124.dist-info/METADATA +0 -192
- {code_puppy-0.0.124.data → code_puppy-0.0.126.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.124.dist-info → code_puppy-0.0.126.dist-info}/licenses/LICENSE +0 -0
|
@@ -590,3 +590,100 @@ def register_command_runner_tools(agent):
|
|
|
590
590
|
- When encountering unexpected situations
|
|
591
591
|
"""
|
|
592
592
|
return share_your_reasoning(context, reasoning, next_steps)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def register_agent_run_shell_command(agent):
|
|
596
|
+
"""Register only the agent_run_shell_command tool."""
|
|
597
|
+
|
|
598
|
+
@agent.tool(strict=False)
|
|
599
|
+
def agent_run_shell_command(
|
|
600
|
+
context: RunContext, command: str = "", cwd: str = None, timeout: int = 60
|
|
601
|
+
) -> ShellCommandOutput:
|
|
602
|
+
"""Execute a shell command with comprehensive monitoring and safety features.
|
|
603
|
+
|
|
604
|
+
This tool provides robust shell command execution with streaming output,
|
|
605
|
+
timeout handling, user confirmation (when not in yolo mode), and proper
|
|
606
|
+
process lifecycle management. Commands are executed in a controlled
|
|
607
|
+
environment with cross-platform process group handling.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
command: The shell command to execute. Cannot be empty or whitespace-only.
|
|
611
|
+
cwd: Working directory for command execution. If None,
|
|
612
|
+
uses the current working directory. Defaults to None.
|
|
613
|
+
timeout: Inactivity timeout in seconds. If no output is
|
|
614
|
+
produced for this duration, the process will be terminated.
|
|
615
|
+
Defaults to 60 seconds.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
ShellCommandOutput: A structured response containing:
|
|
619
|
+
- success (bool): True if command executed successfully (exit code 0)
|
|
620
|
+
- command (str | None): The executed command string
|
|
621
|
+
- error (str | None): Error message if execution failed
|
|
622
|
+
- stdout (str | None): Standard output from the command (last 1000 lines)
|
|
623
|
+
- stderr (str | None): Standard error from the command (last 1000 lines)
|
|
624
|
+
- exit_code (int | None): Process exit code
|
|
625
|
+
- execution_time (float | None): Total execution time in seconds
|
|
626
|
+
- timeout (bool | None): True if command was terminated due to timeout
|
|
627
|
+
- user_interrupted (bool | None): True if user killed the process
|
|
628
|
+
|
|
629
|
+
Examples:
|
|
630
|
+
>>> # Basic command execution
|
|
631
|
+
>>> result = agent_run_shell_command(ctx, "ls -la")
|
|
632
|
+
>>> print(result.stdout)
|
|
633
|
+
|
|
634
|
+
>>> # Command with working directory
|
|
635
|
+
>>> result = agent_run_shell_command(ctx, "npm test", "/path/to/project")
|
|
636
|
+
>>> if result.success:
|
|
637
|
+
... print("Tests passed!")
|
|
638
|
+
|
|
639
|
+
>>> # Command with custom timeout
|
|
640
|
+
>>> result = agent_run_shell_command(ctx, "long_running_command", timeout=300)
|
|
641
|
+
>>> if result.timeout:
|
|
642
|
+
... print("Command timed out")
|
|
643
|
+
|
|
644
|
+
Warning:
|
|
645
|
+
This tool can execute arbitrary shell commands. Exercise caution when
|
|
646
|
+
running untrusted commands, especially those that modify system state.
|
|
647
|
+
"""
|
|
648
|
+
return run_shell_command(context, command, cwd, timeout)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def register_agent_share_your_reasoning(agent):
|
|
652
|
+
"""Register only the agent_share_your_reasoning tool."""
|
|
653
|
+
|
|
654
|
+
@agent.tool(strict=False)
|
|
655
|
+
def agent_share_your_reasoning(
|
|
656
|
+
context: RunContext, reasoning: str = "", next_steps: str | None = None
|
|
657
|
+
) -> ReasoningOutput:
|
|
658
|
+
"""Share the agent's current reasoning and planned next steps with the user.
|
|
659
|
+
|
|
660
|
+
This tool provides transparency into the agent's decision-making process
|
|
661
|
+
by displaying the current reasoning and upcoming actions in a formatted,
|
|
662
|
+
user-friendly manner. It's essential for building trust and understanding
|
|
663
|
+
between the agent and user.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
reasoning: The agent's current thought process, analysis, or
|
|
667
|
+
reasoning for the current situation. This should be clear,
|
|
668
|
+
comprehensive, and explain the 'why' behind decisions.
|
|
669
|
+
next_steps: Planned upcoming actions or steps
|
|
670
|
+
the agent intends to take. Can be None if no specific next steps
|
|
671
|
+
are determined. Defaults to None.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
ReasoningOutput: A simple response object containing:
|
|
675
|
+
- success (bool): Always True, indicating the reasoning was shared
|
|
676
|
+
|
|
677
|
+
Examples:
|
|
678
|
+
>>> reasoning = "I need to analyze the codebase structure first"
|
|
679
|
+
>>> next_steps = "First, I'll list the directory contents, then read key files"
|
|
680
|
+
>>> result = agent_share_your_reasoning(ctx, reasoning, next_steps)
|
|
681
|
+
|
|
682
|
+
Best Practice:
|
|
683
|
+
Use this tool frequently to maintain transparency. Call it:
|
|
684
|
+
- Before starting complex operations
|
|
685
|
+
- When changing strategy or approach
|
|
686
|
+
- To explain why certain decisions are being made
|
|
687
|
+
- When encountering unexpected situations
|
|
688
|
+
"""
|
|
689
|
+
return share_your_reasoning(context, reasoning, next_steps)
|
|
@@ -347,16 +347,19 @@ def _edit_file(
|
|
|
347
347
|
{"content": "full file contents", "overwrite": true}
|
|
348
348
|
{"replacements": [ {"old_str": "foo", "new_str": "bar"}, ... ] }
|
|
349
349
|
{"delete_snippet": "text to remove"}
|
|
350
|
+
|
|
350
351
|
The function auto-detects the payload type and routes to the appropriate internal helper.
|
|
351
352
|
"""
|
|
353
|
+
# Extract file_path from payload
|
|
354
|
+
file_path = os.path.abspath(payload.file_path)
|
|
355
|
+
|
|
352
356
|
# Use provided group_id or generate one if not provided
|
|
353
357
|
if group_id is None:
|
|
354
|
-
group_id = generate_group_id("edit_file",
|
|
358
|
+
group_id = generate_group_id("edit_file", file_path)
|
|
355
359
|
|
|
356
360
|
emit_info(
|
|
357
361
|
"\n[bold white on blue] EDIT FILE [/bold white on blue]", message_group=group_id
|
|
358
362
|
)
|
|
359
|
-
file_path = os.path.abspath(payload.file_path)
|
|
360
363
|
try:
|
|
361
364
|
if isinstance(payload, DeleteSnippetPayload):
|
|
362
365
|
return delete_snippet_from_file(
|
|
@@ -449,9 +452,7 @@ def register_file_modifications_tools(agent):
|
|
|
449
452
|
"""Attach file-editing tools to *agent* with mandatory diff rendering."""
|
|
450
453
|
|
|
451
454
|
@agent.tool(retries=5)
|
|
452
|
-
def edit_file(
|
|
453
|
-
context: RunContext, payload: EditFilePayload | str = ""
|
|
454
|
-
) -> Dict[str, Any]:
|
|
455
|
+
def edit_file(context: RunContext, payload: EditFilePayload) -> Dict[str, Any]:
|
|
455
456
|
"""Comprehensive file editing tool supporting multiple modification strategies.
|
|
456
457
|
|
|
457
458
|
This is the primary file modification tool that supports three distinct editing
|
|
@@ -477,8 +478,9 @@ def register_file_modifications_tools(agent):
|
|
|
477
478
|
DeleteSnippetPayload:
|
|
478
479
|
- delete_snippet (str): Exact text snippet to remove from file
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
file_path (str): Path to the target file. Can be relative or absolute.
|
|
482
|
+
File will be created if it doesn't exist (for ContentPayload).
|
|
483
|
+
|
|
482
484
|
Returns:
|
|
483
485
|
Dict[str, Any]: Operation result containing:
|
|
484
486
|
- success (bool): True if operation completed successfully
|
|
@@ -498,16 +500,16 @@ def register_file_modifications_tools(agent):
|
|
|
498
500
|
Examples:
|
|
499
501
|
>>> # Create new file
|
|
500
502
|
>>> payload = ContentPayload(file_path="foo.py", content="print('Hello World')")
|
|
501
|
-
>>> result = edit_file(payload)
|
|
503
|
+
>>> result = edit_file(context, payload)
|
|
502
504
|
|
|
503
505
|
>>> # Replace specific text
|
|
504
506
|
>>> replacements = [Replacement(old_str="foo", new_str="bar")]
|
|
505
507
|
>>> payload = ReplacementsPayload(file_path="foo.py", replacements=replacements)
|
|
506
|
-
>>> result = edit_file(payload)
|
|
508
|
+
>>> result = edit_file(context, payload)
|
|
507
509
|
|
|
508
510
|
>>> # Delete code block
|
|
509
511
|
>>> payload = DeleteSnippetPayload(file_path="foo.py", delete_snippet="# TODO: remove this")
|
|
510
|
-
>>> result = edit_file(payload)
|
|
512
|
+
>>> result = edit_file(context, payload)
|
|
511
513
|
|
|
512
514
|
Warning:
|
|
513
515
|
- Always verify file contents after modification
|
|
@@ -549,7 +551,7 @@ def register_file_modifications_tools(agent):
|
|
|
549
551
|
return result
|
|
550
552
|
|
|
551
553
|
@agent.tool(retries=5)
|
|
552
|
-
def delete_file(context: RunContext, file_path: str
|
|
554
|
+
def delete_file(context: RunContext, file_path: str) -> Dict[str, Any]:
|
|
553
555
|
"""Safely delete files with comprehensive logging and diff generation.
|
|
554
556
|
|
|
555
557
|
This tool provides safe file deletion with automatic diff generation to show
|
|
@@ -606,3 +608,166 @@ def register_file_modifications_tools(agent):
|
|
|
606
608
|
if "diff" in result:
|
|
607
609
|
del result["diff"]
|
|
608
610
|
return result
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def register_edit_file(agent):
|
|
614
|
+
"""Register only the edit_file tool."""
|
|
615
|
+
|
|
616
|
+
@agent.tool(strict=False)
|
|
617
|
+
def edit_file(
|
|
618
|
+
context: RunContext,
|
|
619
|
+
payload: EditFilePayload | str = "",
|
|
620
|
+
) -> Dict[str, Any]:
|
|
621
|
+
"""Comprehensive file editing tool supporting multiple modification strategies.
|
|
622
|
+
|
|
623
|
+
This is the primary file modification tool that supports three distinct editing
|
|
624
|
+
approaches: full content replacement, targeted text replacements, and snippet
|
|
625
|
+
deletion. It provides robust diff generation, error handling, and automatic
|
|
626
|
+
retry capabilities for reliable file operations.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
context (RunContext): The PydanticAI runtime context for the agent.
|
|
630
|
+
payload: One of three payload types:
|
|
631
|
+
|
|
632
|
+
ContentPayload:
|
|
633
|
+
- content (str): Full file content to write
|
|
634
|
+
- overwrite (bool, optional): Whether to overwrite existing files.
|
|
635
|
+
Defaults to False (safe mode).
|
|
636
|
+
|
|
637
|
+
ReplacementsPayload:
|
|
638
|
+
- replacements (List[Replacement]): List of text replacements where
|
|
639
|
+
each Replacement contains:
|
|
640
|
+
- old_str (str): Exact text to find and replace
|
|
641
|
+
- new_str (str): Replacement text
|
|
642
|
+
|
|
643
|
+
DeleteSnippetPayload:
|
|
644
|
+
- delete_snippet (str): Exact text snippet to remove from file
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Dict[str, Any]: Operation result containing:
|
|
648
|
+
- success (bool): True if operation completed successfully
|
|
649
|
+
- path (str): Absolute path to the modified file
|
|
650
|
+
- message (str): Human-readable description of changes
|
|
651
|
+
- changed (bool): True if file content was actually modified
|
|
652
|
+
- diff (str, optional): Unified diff showing changes made
|
|
653
|
+
- error (str, optional): Error message if operation failed
|
|
654
|
+
|
|
655
|
+
Examples:
|
|
656
|
+
>>> # Create new file with content
|
|
657
|
+
>>> payload = {"file_path": "hello.py", "content": "print('Hello!')"}
|
|
658
|
+
>>> result = edit_file(ctx, payload)
|
|
659
|
+
|
|
660
|
+
>>> # Replace text in existing file
|
|
661
|
+
>>> payload = {
|
|
662
|
+
... "file_path": "config.py",
|
|
663
|
+
... "replacements": [
|
|
664
|
+
... {"old_str": "debug = False", "new_str": "debug = True"}
|
|
665
|
+
... ]
|
|
666
|
+
... }
|
|
667
|
+
>>> result = edit_file(ctx, payload)
|
|
668
|
+
|
|
669
|
+
>>> # Delete snippet from file
|
|
670
|
+
>>> payload = {
|
|
671
|
+
... "file_path": "main.py",
|
|
672
|
+
... "delete_snippet": "# TODO: remove this comment"
|
|
673
|
+
... }
|
|
674
|
+
>>> result = edit_file(ctx, payload)
|
|
675
|
+
|
|
676
|
+
Best Practices:
|
|
677
|
+
- Use replacements for targeted changes (most efficient)
|
|
678
|
+
- Use content payload only for new files or complete rewrites
|
|
679
|
+
- Always check the 'success' field before assuming changes worked
|
|
680
|
+
- Review the 'diff' field to understand what changed
|
|
681
|
+
- Use delete_snippet for removing specific code blocks
|
|
682
|
+
"""
|
|
683
|
+
# Handle string payload parsing (for models that send JSON strings)
|
|
684
|
+
if isinstance(payload, str):
|
|
685
|
+
# Fallback for weird models that just can't help but send json strings...
|
|
686
|
+
payload = json.loads(json_repair.repair_json(payload))
|
|
687
|
+
if "replacements" in payload and "file_path" in payload:
|
|
688
|
+
payload = ReplacementsPayload(**payload)
|
|
689
|
+
elif "delete_snippet" in payload and "file_path" in payload:
|
|
690
|
+
payload = DeleteSnippetPayload(**payload)
|
|
691
|
+
elif "content" in payload and "file_path" in payload:
|
|
692
|
+
payload = ContentPayload(**payload)
|
|
693
|
+
else:
|
|
694
|
+
file_path = "Unknown"
|
|
695
|
+
if "file_path" in payload:
|
|
696
|
+
file_path = payload["file_path"]
|
|
697
|
+
# Diagnose what's missing
|
|
698
|
+
missing = []
|
|
699
|
+
if "file_path" not in payload:
|
|
700
|
+
missing.append("file_path")
|
|
701
|
+
|
|
702
|
+
payload_type = "unknown"
|
|
703
|
+
if "content" in payload:
|
|
704
|
+
payload_type = "content"
|
|
705
|
+
elif "replacements" in payload:
|
|
706
|
+
payload_type = "replacements"
|
|
707
|
+
elif "delete_snippet" in payload:
|
|
708
|
+
payload_type = "delete_snippet"
|
|
709
|
+
else:
|
|
710
|
+
missing.append("content/replacements/delete_snippet")
|
|
711
|
+
|
|
712
|
+
missing_str = ", ".join(missing) if missing else "none"
|
|
713
|
+
return {
|
|
714
|
+
"success": False,
|
|
715
|
+
"path": file_path,
|
|
716
|
+
"message": f"Invalid payload for {payload_type} operation. Missing required fields: {missing_str}. Payload keys: {list(payload.keys())}",
|
|
717
|
+
"changed": False,
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
# Call _edit_file which will extract file_path from payload and handle group_id generation
|
|
721
|
+
result = _edit_file(context, payload)
|
|
722
|
+
if "diff" in result:
|
|
723
|
+
del result["diff"]
|
|
724
|
+
return result
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def register_delete_file(agent):
|
|
728
|
+
"""Register only the delete_file tool."""
|
|
729
|
+
|
|
730
|
+
@agent.tool(strict=False)
|
|
731
|
+
def delete_file(context: RunContext, file_path: str = "") -> Dict[str, Any]:
|
|
732
|
+
"""Safely delete files with comprehensive logging and diff generation.
|
|
733
|
+
|
|
734
|
+
This tool provides safe file deletion with automatic diff generation to show
|
|
735
|
+
exactly what content was removed. It includes proper error handling and
|
|
736
|
+
automatic retry capabilities for reliable operation.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
context (RunContext): The PydanticAI runtime context for the agent.
|
|
740
|
+
file_path (str): Path to the file to delete. Can be relative or absolute.
|
|
741
|
+
Must be an existing regular file (not a directory).
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
Dict[str, Any]: Operation result containing:
|
|
745
|
+
- success (bool): True if file was successfully deleted
|
|
746
|
+
- path (str): Absolute path to the deleted file
|
|
747
|
+
- message (str): Human-readable description of the operation
|
|
748
|
+
- changed (bool): True if file was actually removed
|
|
749
|
+
- error (str, optional): Error message if deletion failed
|
|
750
|
+
|
|
751
|
+
Examples:
|
|
752
|
+
>>> # Delete a specific file
|
|
753
|
+
>>> result = delete_file(ctx, "temp_file.txt")
|
|
754
|
+
>>> if result['success']:
|
|
755
|
+
... print(f"Deleted: {result['path']}")
|
|
756
|
+
|
|
757
|
+
>>> # Handle deletion errors
|
|
758
|
+
>>> result = delete_file(ctx, "missing.txt")
|
|
759
|
+
>>> if not result['success']:
|
|
760
|
+
... print(f"Error: {result.get('error', 'Unknown error')}")
|
|
761
|
+
|
|
762
|
+
Best Practices:
|
|
763
|
+
- Always verify file exists before attempting deletion
|
|
764
|
+
- Check 'success' field to confirm operation completed
|
|
765
|
+
- Use list_files first to confirm file paths
|
|
766
|
+
- Cannot delete directories (use shell commands for that)
|
|
767
|
+
"""
|
|
768
|
+
# Generate group_id for delete_file tool execution
|
|
769
|
+
group_id = generate_group_id("delete_file", file_path)
|
|
770
|
+
result = _delete_file(context, file_path, message_group=group_id)
|
|
771
|
+
if "diff" in result:
|
|
772
|
+
del result["diff"]
|
|
773
|
+
return result
|
|
@@ -173,13 +173,14 @@ def _list_files(
|
|
|
173
173
|
if rel_path == ".":
|
|
174
174
|
rel_path = ""
|
|
175
175
|
if rel_path:
|
|
176
|
-
os.path.join(directory, rel_path)
|
|
176
|
+
dir_path = os.path.join(directory, rel_path)
|
|
177
177
|
results.append(
|
|
178
178
|
ListedFile(
|
|
179
179
|
**{
|
|
180
180
|
"path": rel_path,
|
|
181
181
|
"type": "directory",
|
|
182
182
|
"size": 0,
|
|
183
|
+
"full_path": dir_path,
|
|
183
184
|
"depth": depth,
|
|
184
185
|
}
|
|
185
186
|
)
|
|
@@ -187,6 +188,7 @@ def _list_files(
|
|
|
187
188
|
folder_structure[rel_path] = {
|
|
188
189
|
"path": rel_path,
|
|
189
190
|
"depth": depth,
|
|
191
|
+
"full_path": dir_path,
|
|
190
192
|
}
|
|
191
193
|
for file in files:
|
|
192
194
|
file_path = os.path.join(root, file)
|
|
@@ -199,6 +201,7 @@ def _list_files(
|
|
|
199
201
|
"path": rel_file_path,
|
|
200
202
|
"type": "file",
|
|
201
203
|
"size": size,
|
|
204
|
+
"full_path": file_path,
|
|
202
205
|
"depth": depth,
|
|
203
206
|
}
|
|
204
207
|
results.append(ListedFile(**file_info))
|
|
@@ -621,3 +624,170 @@ def register_file_operations_tools(agent):
|
|
|
621
624
|
- For case-insensitive search, try multiple variants manually
|
|
622
625
|
"""
|
|
623
626
|
return _grep(context, search_string, directory)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def register_list_files(agent):
|
|
630
|
+
"""Register only the list_files tool."""
|
|
631
|
+
|
|
632
|
+
@agent.tool(strict=False)
|
|
633
|
+
def list_files(
|
|
634
|
+
context: RunContext, directory: str = ".", recursive: bool = True
|
|
635
|
+
) -> ListFileOutput:
|
|
636
|
+
"""List files and directories with intelligent filtering and safety features.
|
|
637
|
+
|
|
638
|
+
This tool provides comprehensive directory listing with smart home directory
|
|
639
|
+
detection, project-aware recursion, and token-safe output. It automatically
|
|
640
|
+
ignores common build artifacts, cache directories, and other noise while
|
|
641
|
+
providing rich file metadata and visual formatting.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
context (RunContext): The PydanticAI runtime context for the agent.
|
|
645
|
+
directory (str, optional): Path to the directory to list. Can be relative
|
|
646
|
+
or absolute. Defaults to "." (current directory).
|
|
647
|
+
recursive (bool, optional): Whether to recursively list subdirectories.
|
|
648
|
+
Automatically disabled for home directories unless they contain
|
|
649
|
+
project indicators. Defaults to True.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
ListFileOutput: A structured response containing:
|
|
653
|
+
- files (List[ListedFile]): List of files and directories found, where
|
|
654
|
+
each ListedFile contains:
|
|
655
|
+
- path (str | None): Relative path from the listing directory
|
|
656
|
+
- type (str | None): "file" or "directory"
|
|
657
|
+
- size (int): File size in bytes (0 for directories)
|
|
658
|
+
- full_path (str | None): Absolute path to the item
|
|
659
|
+
- depth (int | None): Nesting depth from the root directory
|
|
660
|
+
- error (str | None): Error message if listing failed
|
|
661
|
+
|
|
662
|
+
Examples:
|
|
663
|
+
>>> # List current directory
|
|
664
|
+
>>> result = list_files(ctx)
|
|
665
|
+
>>> for file in result.files:
|
|
666
|
+
... print(f"{file.type}: {file.path} ({file.size} bytes)")
|
|
667
|
+
|
|
668
|
+
>>> # List specific directory non-recursively
|
|
669
|
+
>>> result = list_files(ctx, "/path/to/project", recursive=False)
|
|
670
|
+
>>> print(f"Found {len(result.files)} items")
|
|
671
|
+
|
|
672
|
+
>>> # Handle potential errors
|
|
673
|
+
>>> result = list_files(ctx, "/nonexistent/path")
|
|
674
|
+
>>> if result.error:
|
|
675
|
+
... print(f"Error: {result.error}")
|
|
676
|
+
|
|
677
|
+
Best Practices:
|
|
678
|
+
- Always use this before reading/modifying files
|
|
679
|
+
- Use non-recursive for quick directory overviews
|
|
680
|
+
- Check for errors in the response
|
|
681
|
+
- Combine with grep to find specific file patterns
|
|
682
|
+
"""
|
|
683
|
+
return _list_files(context, directory, recursive)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def register_read_file(agent):
|
|
687
|
+
"""Register only the read_file tool."""
|
|
688
|
+
|
|
689
|
+
@agent.tool(strict=False)
|
|
690
|
+
def read_file(
|
|
691
|
+
context: RunContext,
|
|
692
|
+
file_path: str = "",
|
|
693
|
+
start_line: int | None = None,
|
|
694
|
+
num_lines: int | None = None,
|
|
695
|
+
) -> ReadFileOutput:
|
|
696
|
+
"""Read file contents with optional line-range selection and token safety.
|
|
697
|
+
|
|
698
|
+
This tool provides safe file reading with automatic token counting and
|
|
699
|
+
optional line-range selection for handling large files efficiently.
|
|
700
|
+
It protects against reading excessively large files that could overwhelm
|
|
701
|
+
the agent's context window.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
context (RunContext): The PydanticAI runtime context for the agent.
|
|
705
|
+
file_path (str): Path to the file to read. Can be relative or absolute.
|
|
706
|
+
Cannot be empty.
|
|
707
|
+
start_line (int | None, optional): Starting line number for partial reads
|
|
708
|
+
(1-based indexing). If specified, num_lines must also be provided.
|
|
709
|
+
Defaults to None (read entire file).
|
|
710
|
+
num_lines (int | None, optional): Number of lines to read starting from
|
|
711
|
+
start_line. Must be specified if start_line is provided.
|
|
712
|
+
Defaults to None (read to end of file).
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
ReadFileOutput: A structured response containing:
|
|
716
|
+
- content (str | None): The file contents or error message
|
|
717
|
+
- num_tokens (int): Estimated token count (constrained to < 10,000)
|
|
718
|
+
- error (str | None): Error message if reading failed
|
|
719
|
+
|
|
720
|
+
Examples:
|
|
721
|
+
>>> # Read entire file
|
|
722
|
+
>>> result = read_file(ctx, "example.py")
|
|
723
|
+
>>> print(f"Read {result.num_tokens} tokens")
|
|
724
|
+
>>> print(result.content)
|
|
725
|
+
|
|
726
|
+
>>> # Read specific line range
|
|
727
|
+
>>> result = read_file(ctx, "large_file.py", start_line=10, num_lines=20)
|
|
728
|
+
>>> print("Lines 10-29:", result.content)
|
|
729
|
+
|
|
730
|
+
>>> # Handle errors
|
|
731
|
+
>>> result = read_file(ctx, "missing.txt")
|
|
732
|
+
>>> if result.error:
|
|
733
|
+
... print(f"Error: {result.error}")
|
|
734
|
+
|
|
735
|
+
Best Practices:
|
|
736
|
+
- Always check for errors before using content
|
|
737
|
+
- Use line ranges for large files to avoid token limits
|
|
738
|
+
- Monitor num_tokens to stay within context limits
|
|
739
|
+
- Combine with list_files to find files first
|
|
740
|
+
"""
|
|
741
|
+
return _read_file(context, file_path, start_line, num_lines)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def register_grep(agent):
|
|
745
|
+
"""Register only the grep tool."""
|
|
746
|
+
|
|
747
|
+
@agent.tool(strict=False)
|
|
748
|
+
def grep(
|
|
749
|
+
context: RunContext, search_string: str = "", directory: str = "."
|
|
750
|
+
) -> GrepOutput:
|
|
751
|
+
"""Recursively search for text patterns across files with intelligent filtering.
|
|
752
|
+
|
|
753
|
+
This tool provides powerful text searching across directory trees with
|
|
754
|
+
automatic filtering of irrelevant files, binary detection, and match limiting
|
|
755
|
+
for performance. It's essential for code exploration and finding specific
|
|
756
|
+
patterns or references.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
context (RunContext): The PydanticAI runtime context for the agent.
|
|
760
|
+
search_string (str): The text pattern to search for. Performs exact
|
|
761
|
+
string matching (not regex). Cannot be empty.
|
|
762
|
+
directory (str, optional): Root directory to start the recursive search.
|
|
763
|
+
Can be relative or absolute. Defaults to "." (current directory).
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
GrepOutput: A structured response containing:
|
|
767
|
+
- matches (List[MatchInfo]): List of matches found, where each
|
|
768
|
+
MatchInfo contains:
|
|
769
|
+
- file_path (str | None): Absolute path to the file containing the match
|
|
770
|
+
- line_number (int | None): Line number where match was found (1-based)
|
|
771
|
+
- line_content (str | None): Full line content containing the match
|
|
772
|
+
|
|
773
|
+
Examples:
|
|
774
|
+
>>> # Search for function definitions
|
|
775
|
+
>>> result = grep(ctx, "def my_function")
|
|
776
|
+
>>> for match in result.matches:
|
|
777
|
+
... print(f"{match.file_path}:{match.line_number}: {match.line_content}")
|
|
778
|
+
|
|
779
|
+
>>> # Search in specific directory
|
|
780
|
+
>>> result = grep(ctx, "TODO", "/path/to/project/src")
|
|
781
|
+
>>> print(f"Found {len(result.matches)} TODO items")
|
|
782
|
+
|
|
783
|
+
>>> # Search for imports
|
|
784
|
+
>>> result = grep(ctx, "import pandas")
|
|
785
|
+
>>> files_using_pandas = {match.file_path for match in result.matches}
|
|
786
|
+
|
|
787
|
+
Best Practices:
|
|
788
|
+
- Use specific search terms to avoid too many results
|
|
789
|
+
- Search is case-sensitive; try variations if needed
|
|
790
|
+
- Combine with read_file to examine matches in detail
|
|
791
|
+
- For case-insensitive search, try multiple variants manually
|
|
792
|
+
"""
|
|
793
|
+
return _grep(context, search_string, directory)
|