patchpal 0.4.4__tar.gz → 0.5.0__tar.gz
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.
- {patchpal-0.4.4/patchpal.egg-info → patchpal-0.5.0}/PKG-INFO +166 -5
- {patchpal-0.4.4 → patchpal-0.5.0}/README.md +165 -4
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/__init__.py +1 -1
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/agent.py +108 -9
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/permissions.py +41 -3
- patchpal-0.5.0/patchpal/tool_schema.py +154 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/tools.py +181 -6
- {patchpal-0.4.4 → patchpal-0.5.0/patchpal.egg-info}/PKG-INFO +166 -5
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal.egg-info/SOURCES.txt +3 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_agent.py +206 -0
- patchpal-0.5.0/tests/test_custom_tools.py +76 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_guardrails.py +233 -57
- patchpal-0.5.0/tests/test_permissions.py +302 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_tools.py +29 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/LICENSE +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/MANIFEST.in +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/cli.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/context.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/skills.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal/system_prompt.md +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/pyproject.toml +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/setup.cfg +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_cli.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_context.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_operational_safety.py +0 -0
- {patchpal-0.4.4 → patchpal-0.5.0}/tests/test_skills.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchpal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A lean Claude Code clone in pure Python
|
|
5
5
|
Author: PatchPal Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,6 +46,18 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
A key goal of this project is to approximate Claude Code's core functionality while remaining lean, accessible, and configurable, enabling learning, experimentation, and broad applicability across use cases.
|
|
48
48
|
|
|
49
|
+
```bash
|
|
50
|
+
$ls ./patchpal
|
|
51
|
+
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tool_schema.py tools.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
$ pip install patchpal # install
|
|
58
|
+
$ patchpal # start
|
|
59
|
+
```
|
|
60
|
+
|
|
49
61
|
## Table of Contents
|
|
50
62
|
|
|
51
63
|
- [Installation](https://github.com/amaiya/patchpal?tab=readme-ov-file#installation)
|
|
@@ -64,6 +76,7 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
64
76
|
- [Air-Gapped and Offline Environments](https://github.com/amaiya/patchpal?tab=readme-ov-file#air-gapped-and-offline-environments)
|
|
65
77
|
- [Maximum Security Mode](https://github.com/amaiya/patchpal?tab=readme-ov-file#maximum-security-mode)
|
|
66
78
|
- [Usage](https://github.com/amaiya/patchpal?tab=readme-ov-file#usage)
|
|
79
|
+
- [Python API](https://github.com/amaiya/patchpal?tab=readme-ov-file#python-api)
|
|
67
80
|
- [Configuration](https://github.com/amaiya/patchpal?tab=readme-ov-file#configuration)
|
|
68
81
|
- [Example Tasks](https://github.com/amaiya/patchpal?tab=readme-ov-file#example-tasks)
|
|
69
82
|
- [Safety](https://github.com/amaiya/patchpal?tab=readme-ov-file#safety)
|
|
@@ -71,10 +84,6 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
71
84
|
- [Troubleshooting](https://github.com/amaiya/patchpal?tab=readme-ov-file#troubleshooting)
|
|
72
85
|
|
|
73
86
|
|
|
74
|
-
```bash
|
|
75
|
-
$ls ./patchpal
|
|
76
|
-
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tools.py
|
|
77
|
-
```
|
|
78
87
|
|
|
79
88
|
## Installation
|
|
80
89
|
|
|
@@ -668,6 +677,158 @@ The agent will process your request and show you the results. You can continue w
|
|
|
668
677
|
- **Interrupt Agent**: Press `Ctrl-C` during agent execution to stop the current task without exiting PatchPal
|
|
669
678
|
- **Exit**: Type `exit`, `quit`, or press `Ctrl-C` at the prompt to exit PatchPal
|
|
670
679
|
|
|
680
|
+
## Python API
|
|
681
|
+
|
|
682
|
+
PatchPal can be used programmatically from Python scripts or a REPL, giving you full agent capabilities with a simple API. **Unlike fully autonomous agent frameworks, PatchPal is designed for human-in-the-loop workflows** where users maintain control through interactive permission prompts, making it ideal for code assistance, debugging, and automation tasks that benefit from human oversight.
|
|
683
|
+
|
|
684
|
+
**Basic Usage:**
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
from patchpal.agent import create_agent
|
|
688
|
+
|
|
689
|
+
# Create an agent (uses default model or PATCHPAL_MODEL env var)
|
|
690
|
+
agent = create_agent()
|
|
691
|
+
|
|
692
|
+
# Or specify a model explicitly
|
|
693
|
+
agent = create_agent(model_id="anthropic/claude-sonnet-4_5")
|
|
694
|
+
|
|
695
|
+
# Run the agent on a task
|
|
696
|
+
response = agent.run("List all Python files in this directory")
|
|
697
|
+
print(response)
|
|
698
|
+
|
|
699
|
+
# Continue the conversation (history is maintained)
|
|
700
|
+
response = agent.run("Now read the main agent file")
|
|
701
|
+
print(response)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**Adding Custom Tools:**
|
|
705
|
+
|
|
706
|
+
One advantage of the Python API, is that it can easily be used with custom tools that you define as Python functions. Tool schemas are auto-generated from Python functions with type hints and docstrings:
|
|
707
|
+
|
|
708
|
+
```python
|
|
709
|
+
from patchpal.agent import create_agent
|
|
710
|
+
|
|
711
|
+
def calculator(x: int, y: int, operation: str = "add") -> str:
|
|
712
|
+
"""Perform basic arithmetic operations.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
x: First number
|
|
716
|
+
y: Second number
|
|
717
|
+
operation: Operation to perform (add, subtract, multiply, divide)
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
Result as a string
|
|
721
|
+
"""
|
|
722
|
+
if operation == "add":
|
|
723
|
+
return f"{x} + {y} = {x + y}"
|
|
724
|
+
elif operation == "subtract":
|
|
725
|
+
return f"{x} - {y} = {x - y}"
|
|
726
|
+
elif operation == "multiply":
|
|
727
|
+
return f"{x} * {y} = {x * y}"
|
|
728
|
+
elif operation == "divide":
|
|
729
|
+
if y == 0:
|
|
730
|
+
return "Error: Cannot divide by zero"
|
|
731
|
+
return f"{x} / {y} = {x / y}"
|
|
732
|
+
return "Unknown operation"
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
736
|
+
"""Get weather information for a city.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
city: Name of the city
|
|
740
|
+
units: Temperature units (celsius or fahrenheit)
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
Weather information string
|
|
744
|
+
"""
|
|
745
|
+
# Your implementation here (API call, etc.)
|
|
746
|
+
return f"Weather in {city}: 22°{units[0].upper()}, Sunny"
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# Create agent with custom tools
|
|
750
|
+
agent = create_agent(
|
|
751
|
+
model_id="anthropic/claude-sonnet-4-5",
|
|
752
|
+
custom_tools=[calculator, get_weather]
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# Use the agent - it will call your custom tools when appropriate
|
|
756
|
+
response = agent.run("What's 15 multiplied by 23?")
|
|
757
|
+
print(response)
|
|
758
|
+
|
|
759
|
+
response = agent.run("What's the weather in Paris?")
|
|
760
|
+
print(response)
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Key Points:**
|
|
764
|
+
- Custom tools are automatically converted to LLM tool schemas
|
|
765
|
+
- Functions should have type hints and Google-style docstrings
|
|
766
|
+
- The agent will call your functions when appropriate
|
|
767
|
+
- Tool execution follows the same permission system as built-in tools
|
|
768
|
+
|
|
769
|
+
**Advanced Usage:**
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
from patchpal.agent import PatchPalAgent
|
|
773
|
+
|
|
774
|
+
# Create agent with custom configuration
|
|
775
|
+
agent = PatchPalAgent(model_id="anthropic/claude-sonnet-4-5")
|
|
776
|
+
|
|
777
|
+
# Set custom max iterations for complex tasks
|
|
778
|
+
response = agent.run("Refactor the entire codebase", max_iterations=200)
|
|
779
|
+
|
|
780
|
+
# Access conversation history
|
|
781
|
+
print(f"Messages in history: {len(agent.messages)}")
|
|
782
|
+
|
|
783
|
+
# Check context window usage
|
|
784
|
+
stats = agent.context_manager.get_usage_stats(agent.messages)
|
|
785
|
+
print(f"Token usage: {stats['total_tokens']:,} / {stats['context_limit']:,}")
|
|
786
|
+
print(f"Usage: {stats['usage_percent']}%")
|
|
787
|
+
|
|
788
|
+
# Manually trigger compaction if needed
|
|
789
|
+
if agent.context_manager.needs_compaction(agent.messages):
|
|
790
|
+
agent._perform_auto_compaction()
|
|
791
|
+
|
|
792
|
+
# Track API costs (cumulative token counts across session)
|
|
793
|
+
print(f"Total LLM calls: {agent.total_llm_calls}")
|
|
794
|
+
print(f"Cumulative input tokens: {agent.cumulative_input_tokens:,}")
|
|
795
|
+
print(f"Cumulative output tokens: {agent.cumulative_output_tokens:,}")
|
|
796
|
+
print(f"Total tokens: {agent.cumulative_input_tokens + agent.cumulative_output_tokens:,}")
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Use Cases:**
|
|
800
|
+
- **Interactive debugging**: Use in Jupyter notebooks for hands-on debugging with agent assistance
|
|
801
|
+
- **Automation scripts**: Build scripts that use the agent for complex tasks with human oversight
|
|
802
|
+
- **Custom workflows**: Integrate PatchPal into your own tools and pipelines
|
|
803
|
+
- **Code review assistance**: Programmatic code analysis with permission controls
|
|
804
|
+
- **Batch processing**: Process multiple tasks programmatically while maintaining control
|
|
805
|
+
- **Testing and evaluation**: Test agent behavior with different prompts and configurations
|
|
806
|
+
|
|
807
|
+
**Key Features:**
|
|
808
|
+
- **Human-in-the-loop design**: Permission prompts ensure human oversight (unlike fully autonomous frameworks)
|
|
809
|
+
- **Stateful conversations**: Agent maintains full conversation history
|
|
810
|
+
- **Custom tools**: Add your own Python functions as tools with automatic schema generation
|
|
811
|
+
- **Automatic context management**: Auto-compaction works the same as CLI
|
|
812
|
+
- **All built-in tools available**: File operations, git, web search, skills, etc.
|
|
813
|
+
- **Model flexibility**: Works with any LiteLLM-compatible model
|
|
814
|
+
- **Token tracking**: Monitor API usage and costs in real-time
|
|
815
|
+
- **Environment variables respected**: All `PATCHPAL_*` settings apply
|
|
816
|
+
|
|
817
|
+
**PatchPal vs. Other Agent Frameworks:**
|
|
818
|
+
|
|
819
|
+
Unlike fully autonomous agent frameworks (e.g., smolagents, autogen), PatchPal is explicitly designed for **human-in-the-loop workflows**:
|
|
820
|
+
|
|
821
|
+
| Feature | PatchPal | Autonomous Frameworks |
|
|
822
|
+
|---------|----------|----------------------|
|
|
823
|
+
| **Design Philosophy** | Human oversight & control | Autonomous execution |
|
|
824
|
+
| **Permission System** | Interactive prompts for sensitive operations | Typically no prompts |
|
|
825
|
+
| **Primary Use Case** | Code assistance, debugging, interactive tasks | Automated workflows, batch processing |
|
|
826
|
+
| **Safety Model** | Write boundary protection, command blocking | Varies by framework |
|
|
827
|
+
| **Custom Tools** | Yes, with automatic schema generation | Yes (varies by framework) |
|
|
828
|
+
| **Best For** | Developers who want AI assistance with control | Automation, research, agent benchmarks |
|
|
829
|
+
|
|
830
|
+
The Python API uses the same agent implementation as the CLI, so you get the complete feature set including permissions, safety guardrails, and context management.
|
|
831
|
+
|
|
671
832
|
## Configuration
|
|
672
833
|
|
|
673
834
|
PatchPal can be configured through `PATCHPAL_*` environment variables to customize behavior, security, and performance.
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
A key goal of this project is to approximate Claude Code's core functionality while remaining lean, accessible, and configurable, enabling learning, experimentation, and broad applicability across use cases.
|
|
11
11
|
|
|
12
|
+
```bash
|
|
13
|
+
$ls ./patchpal
|
|
14
|
+
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tool_schema.py tools.py
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
$ pip install patchpal # install
|
|
21
|
+
$ patchpal # start
|
|
22
|
+
```
|
|
23
|
+
|
|
12
24
|
## Table of Contents
|
|
13
25
|
|
|
14
26
|
- [Installation](https://github.com/amaiya/patchpal?tab=readme-ov-file#installation)
|
|
@@ -27,6 +39,7 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
27
39
|
- [Air-Gapped and Offline Environments](https://github.com/amaiya/patchpal?tab=readme-ov-file#air-gapped-and-offline-environments)
|
|
28
40
|
- [Maximum Security Mode](https://github.com/amaiya/patchpal?tab=readme-ov-file#maximum-security-mode)
|
|
29
41
|
- [Usage](https://github.com/amaiya/patchpal?tab=readme-ov-file#usage)
|
|
42
|
+
- [Python API](https://github.com/amaiya/patchpal?tab=readme-ov-file#python-api)
|
|
30
43
|
- [Configuration](https://github.com/amaiya/patchpal?tab=readme-ov-file#configuration)
|
|
31
44
|
- [Example Tasks](https://github.com/amaiya/patchpal?tab=readme-ov-file#example-tasks)
|
|
32
45
|
- [Safety](https://github.com/amaiya/patchpal?tab=readme-ov-file#safety)
|
|
@@ -34,10 +47,6 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
34
47
|
- [Troubleshooting](https://github.com/amaiya/patchpal?tab=readme-ov-file#troubleshooting)
|
|
35
48
|
|
|
36
49
|
|
|
37
|
-
```bash
|
|
38
|
-
$ls ./patchpal
|
|
39
|
-
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tools.py
|
|
40
|
-
```
|
|
41
50
|
|
|
42
51
|
## Installation
|
|
43
52
|
|
|
@@ -631,6 +640,158 @@ The agent will process your request and show you the results. You can continue w
|
|
|
631
640
|
- **Interrupt Agent**: Press `Ctrl-C` during agent execution to stop the current task without exiting PatchPal
|
|
632
641
|
- **Exit**: Type `exit`, `quit`, or press `Ctrl-C` at the prompt to exit PatchPal
|
|
633
642
|
|
|
643
|
+
## Python API
|
|
644
|
+
|
|
645
|
+
PatchPal can be used programmatically from Python scripts or a REPL, giving you full agent capabilities with a simple API. **Unlike fully autonomous agent frameworks, PatchPal is designed for human-in-the-loop workflows** where users maintain control through interactive permission prompts, making it ideal for code assistance, debugging, and automation tasks that benefit from human oversight.
|
|
646
|
+
|
|
647
|
+
**Basic Usage:**
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
from patchpal.agent import create_agent
|
|
651
|
+
|
|
652
|
+
# Create an agent (uses default model or PATCHPAL_MODEL env var)
|
|
653
|
+
agent = create_agent()
|
|
654
|
+
|
|
655
|
+
# Or specify a model explicitly
|
|
656
|
+
agent = create_agent(model_id="anthropic/claude-sonnet-4_5")
|
|
657
|
+
|
|
658
|
+
# Run the agent on a task
|
|
659
|
+
response = agent.run("List all Python files in this directory")
|
|
660
|
+
print(response)
|
|
661
|
+
|
|
662
|
+
# Continue the conversation (history is maintained)
|
|
663
|
+
response = agent.run("Now read the main agent file")
|
|
664
|
+
print(response)
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Adding Custom Tools:**
|
|
668
|
+
|
|
669
|
+
One advantage of the Python API, is that it can easily be used with custom tools that you define as Python functions. Tool schemas are auto-generated from Python functions with type hints and docstrings:
|
|
670
|
+
|
|
671
|
+
```python
|
|
672
|
+
from patchpal.agent import create_agent
|
|
673
|
+
|
|
674
|
+
def calculator(x: int, y: int, operation: str = "add") -> str:
|
|
675
|
+
"""Perform basic arithmetic operations.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
x: First number
|
|
679
|
+
y: Second number
|
|
680
|
+
operation: Operation to perform (add, subtract, multiply, divide)
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Result as a string
|
|
684
|
+
"""
|
|
685
|
+
if operation == "add":
|
|
686
|
+
return f"{x} + {y} = {x + y}"
|
|
687
|
+
elif operation == "subtract":
|
|
688
|
+
return f"{x} - {y} = {x - y}"
|
|
689
|
+
elif operation == "multiply":
|
|
690
|
+
return f"{x} * {y} = {x * y}"
|
|
691
|
+
elif operation == "divide":
|
|
692
|
+
if y == 0:
|
|
693
|
+
return "Error: Cannot divide by zero"
|
|
694
|
+
return f"{x} / {y} = {x / y}"
|
|
695
|
+
return "Unknown operation"
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
699
|
+
"""Get weather information for a city.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
city: Name of the city
|
|
703
|
+
units: Temperature units (celsius or fahrenheit)
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Weather information string
|
|
707
|
+
"""
|
|
708
|
+
# Your implementation here (API call, etc.)
|
|
709
|
+
return f"Weather in {city}: 22°{units[0].upper()}, Sunny"
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# Create agent with custom tools
|
|
713
|
+
agent = create_agent(
|
|
714
|
+
model_id="anthropic/claude-sonnet-4-5",
|
|
715
|
+
custom_tools=[calculator, get_weather]
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Use the agent - it will call your custom tools when appropriate
|
|
719
|
+
response = agent.run("What's 15 multiplied by 23?")
|
|
720
|
+
print(response)
|
|
721
|
+
|
|
722
|
+
response = agent.run("What's the weather in Paris?")
|
|
723
|
+
print(response)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
**Key Points:**
|
|
727
|
+
- Custom tools are automatically converted to LLM tool schemas
|
|
728
|
+
- Functions should have type hints and Google-style docstrings
|
|
729
|
+
- The agent will call your functions when appropriate
|
|
730
|
+
- Tool execution follows the same permission system as built-in tools
|
|
731
|
+
|
|
732
|
+
**Advanced Usage:**
|
|
733
|
+
|
|
734
|
+
```python
|
|
735
|
+
from patchpal.agent import PatchPalAgent
|
|
736
|
+
|
|
737
|
+
# Create agent with custom configuration
|
|
738
|
+
agent = PatchPalAgent(model_id="anthropic/claude-sonnet-4-5")
|
|
739
|
+
|
|
740
|
+
# Set custom max iterations for complex tasks
|
|
741
|
+
response = agent.run("Refactor the entire codebase", max_iterations=200)
|
|
742
|
+
|
|
743
|
+
# Access conversation history
|
|
744
|
+
print(f"Messages in history: {len(agent.messages)}")
|
|
745
|
+
|
|
746
|
+
# Check context window usage
|
|
747
|
+
stats = agent.context_manager.get_usage_stats(agent.messages)
|
|
748
|
+
print(f"Token usage: {stats['total_tokens']:,} / {stats['context_limit']:,}")
|
|
749
|
+
print(f"Usage: {stats['usage_percent']}%")
|
|
750
|
+
|
|
751
|
+
# Manually trigger compaction if needed
|
|
752
|
+
if agent.context_manager.needs_compaction(agent.messages):
|
|
753
|
+
agent._perform_auto_compaction()
|
|
754
|
+
|
|
755
|
+
# Track API costs (cumulative token counts across session)
|
|
756
|
+
print(f"Total LLM calls: {agent.total_llm_calls}")
|
|
757
|
+
print(f"Cumulative input tokens: {agent.cumulative_input_tokens:,}")
|
|
758
|
+
print(f"Cumulative output tokens: {agent.cumulative_output_tokens:,}")
|
|
759
|
+
print(f"Total tokens: {agent.cumulative_input_tokens + agent.cumulative_output_tokens:,}")
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Use Cases:**
|
|
763
|
+
- **Interactive debugging**: Use in Jupyter notebooks for hands-on debugging with agent assistance
|
|
764
|
+
- **Automation scripts**: Build scripts that use the agent for complex tasks with human oversight
|
|
765
|
+
- **Custom workflows**: Integrate PatchPal into your own tools and pipelines
|
|
766
|
+
- **Code review assistance**: Programmatic code analysis with permission controls
|
|
767
|
+
- **Batch processing**: Process multiple tasks programmatically while maintaining control
|
|
768
|
+
- **Testing and evaluation**: Test agent behavior with different prompts and configurations
|
|
769
|
+
|
|
770
|
+
**Key Features:**
|
|
771
|
+
- **Human-in-the-loop design**: Permission prompts ensure human oversight (unlike fully autonomous frameworks)
|
|
772
|
+
- **Stateful conversations**: Agent maintains full conversation history
|
|
773
|
+
- **Custom tools**: Add your own Python functions as tools with automatic schema generation
|
|
774
|
+
- **Automatic context management**: Auto-compaction works the same as CLI
|
|
775
|
+
- **All built-in tools available**: File operations, git, web search, skills, etc.
|
|
776
|
+
- **Model flexibility**: Works with any LiteLLM-compatible model
|
|
777
|
+
- **Token tracking**: Monitor API usage and costs in real-time
|
|
778
|
+
- **Environment variables respected**: All `PATCHPAL_*` settings apply
|
|
779
|
+
|
|
780
|
+
**PatchPal vs. Other Agent Frameworks:**
|
|
781
|
+
|
|
782
|
+
Unlike fully autonomous agent frameworks (e.g., smolagents, autogen), PatchPal is explicitly designed for **human-in-the-loop workflows**:
|
|
783
|
+
|
|
784
|
+
| Feature | PatchPal | Autonomous Frameworks |
|
|
785
|
+
|---------|----------|----------------------|
|
|
786
|
+
| **Design Philosophy** | Human oversight & control | Autonomous execution |
|
|
787
|
+
| **Permission System** | Interactive prompts for sensitive operations | Typically no prompts |
|
|
788
|
+
| **Primary Use Case** | Code assistance, debugging, interactive tasks | Automated workflows, batch processing |
|
|
789
|
+
| **Safety Model** | Write boundary protection, command blocking | Varies by framework |
|
|
790
|
+
| **Custom Tools** | Yes, with automatic schema generation | Yes (varies by framework) |
|
|
791
|
+
| **Best For** | Developers who want AI assistance with control | Automation, research, agent benchmarks |
|
|
792
|
+
|
|
793
|
+
The Python API uses the same agent implementation as the CLI, so you get the complete feature set including permissions, safety guardrails, and context management.
|
|
794
|
+
|
|
634
795
|
## Configuration
|
|
635
796
|
|
|
636
797
|
PatchPal can be configured through `PATCHPAL_*` environment variables to customize behavior, security, and performance.
|
|
@@ -811,12 +811,17 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
811
811
|
class PatchPalAgent:
|
|
812
812
|
"""Simple agent that uses LiteLLM for tool calling."""
|
|
813
813
|
|
|
814
|
-
def __init__(self, model_id: str = "anthropic/claude-sonnet-4-5"):
|
|
814
|
+
def __init__(self, model_id: str = "anthropic/claude-sonnet-4-5", custom_tools=None):
|
|
815
815
|
"""Initialize the agent.
|
|
816
816
|
|
|
817
817
|
Args:
|
|
818
818
|
model_id: LiteLLM model identifier
|
|
819
|
+
custom_tools: Optional list of Python functions to add as tools
|
|
819
820
|
"""
|
|
821
|
+
# Store custom tools
|
|
822
|
+
self.custom_tools = custom_tools or []
|
|
823
|
+
self.custom_tool_funcs = {func.__name__: func for func in self.custom_tools}
|
|
824
|
+
|
|
820
825
|
# Convert ollama/ to ollama_chat/ for LiteLLM compatibility
|
|
821
826
|
if model_id.startswith("ollama/"):
|
|
822
827
|
model_id = model_id.replace("ollama/", "ollama_chat/", 1)
|
|
@@ -1029,6 +1034,67 @@ class PatchPalAgent:
|
|
|
1029
1034
|
if self.enable_auto_compact and self.context_manager.needs_compaction(self.messages):
|
|
1030
1035
|
self._perform_auto_compaction()
|
|
1031
1036
|
|
|
1037
|
+
# Agent loop with interrupt handling
|
|
1038
|
+
try:
|
|
1039
|
+
return self._run_agent_loop(max_iterations)
|
|
1040
|
+
except KeyboardInterrupt:
|
|
1041
|
+
# Clean up conversation state if interrupted mid-execution
|
|
1042
|
+
self._cleanup_interrupted_state()
|
|
1043
|
+
raise # Re-raise so CLI can handle it
|
|
1044
|
+
|
|
1045
|
+
def _cleanup_interrupted_state(self):
|
|
1046
|
+
"""Clean up conversation state after KeyboardInterrupt.
|
|
1047
|
+
|
|
1048
|
+
If the last message is an assistant message with tool_calls but no
|
|
1049
|
+
corresponding tool responses, we need to either remove the message
|
|
1050
|
+
or add error responses to maintain valid conversation structure.
|
|
1051
|
+
"""
|
|
1052
|
+
if not self.messages:
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
last_msg = self.messages[-1]
|
|
1056
|
+
|
|
1057
|
+
# Check if last message is assistant with tool_calls
|
|
1058
|
+
if last_msg.get("role") == "assistant" and last_msg.get("tool_calls"):
|
|
1059
|
+
tool_calls = last_msg["tool_calls"]
|
|
1060
|
+
|
|
1061
|
+
# Check if we have tool responses for all tool_calls
|
|
1062
|
+
tool_call_ids = {tc.id for tc in tool_calls}
|
|
1063
|
+
|
|
1064
|
+
# Look for tool responses after this assistant message
|
|
1065
|
+
# (should be immediately following, but scan to be safe)
|
|
1066
|
+
response_ids = set()
|
|
1067
|
+
for msg in self.messages[self.messages.index(last_msg) + 1 :]:
|
|
1068
|
+
if msg.get("role") == "tool":
|
|
1069
|
+
response_ids.add(msg.get("tool_call_id"))
|
|
1070
|
+
|
|
1071
|
+
# If we're missing responses, add error responses for all tool calls
|
|
1072
|
+
if tool_call_ids != response_ids:
|
|
1073
|
+
missing_ids = tool_call_ids - response_ids
|
|
1074
|
+
|
|
1075
|
+
# Add error tool responses for the missing tool calls
|
|
1076
|
+
for tool_call in tool_calls:
|
|
1077
|
+
if tool_call.id in missing_ids:
|
|
1078
|
+
self.messages.append(
|
|
1079
|
+
{
|
|
1080
|
+
"role": "tool",
|
|
1081
|
+
"tool_call_id": tool_call.id,
|
|
1082
|
+
"name": tool_call.function.name,
|
|
1083
|
+
"content": "Error: Operation interrupted by user (Ctrl-C)",
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
def _run_agent_loop(self, max_iterations: int) -> str:
|
|
1088
|
+
"""Internal method that runs the agent loop.
|
|
1089
|
+
|
|
1090
|
+
Separated from run() to enable proper interrupt handling.
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
max_iterations: Maximum number of agent iterations
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
The agent's final response
|
|
1097
|
+
"""
|
|
1032
1098
|
# Agent loop
|
|
1033
1099
|
for iteration in range(max_iterations):
|
|
1034
1100
|
# Show thinking message
|
|
@@ -1042,10 +1108,18 @@ class PatchPalAgent:
|
|
|
1042
1108
|
|
|
1043
1109
|
# Use LiteLLM for all providers
|
|
1044
1110
|
try:
|
|
1111
|
+
# Build tool list (built-in + custom)
|
|
1112
|
+
tools = list(TOOLS)
|
|
1113
|
+
if self.custom_tools:
|
|
1114
|
+
from patchpal.tool_schema import function_to_tool_schema
|
|
1115
|
+
|
|
1116
|
+
for func in self.custom_tools:
|
|
1117
|
+
tools.append(function_to_tool_schema(func))
|
|
1118
|
+
|
|
1045
1119
|
response = litellm.completion(
|
|
1046
1120
|
model=self.model_id,
|
|
1047
1121
|
messages=messages,
|
|
1048
|
-
tools=
|
|
1122
|
+
tools=tools,
|
|
1049
1123
|
tool_choice="auto",
|
|
1050
1124
|
**self.litellm_kwargs,
|
|
1051
1125
|
)
|
|
@@ -1099,15 +1173,25 @@ class PatchPalAgent:
|
|
|
1099
1173
|
tool_result = f"Error: Invalid JSON arguments for {tool_name}"
|
|
1100
1174
|
print(f"\033[1;31m✗ {tool_name}: Invalid arguments\033[0m")
|
|
1101
1175
|
else:
|
|
1102
|
-
# Get the tool function
|
|
1103
|
-
tool_func =
|
|
1176
|
+
# Get the tool function (check custom tools first, then built-in)
|
|
1177
|
+
tool_func = self.custom_tool_funcs.get(tool_name) or TOOL_FUNCTIONS.get(
|
|
1178
|
+
tool_name
|
|
1179
|
+
)
|
|
1104
1180
|
if tool_func is None:
|
|
1105
1181
|
tool_result = f"Error: Unknown tool {tool_name}"
|
|
1106
1182
|
print(f"\033[1;31m✗ Unknown tool: {tool_name}\033[0m")
|
|
1107
1183
|
else:
|
|
1108
1184
|
# Show tool call message
|
|
1109
|
-
|
|
1110
|
-
|
|
1185
|
+
if tool_name in self.custom_tool_funcs:
|
|
1186
|
+
# Custom tool - show generic message with args
|
|
1187
|
+
args_preview = str(tool_args)[:60]
|
|
1188
|
+
if len(str(tool_args)) > 60:
|
|
1189
|
+
args_preview += "..."
|
|
1190
|
+
print(
|
|
1191
|
+
f"\033[2m🔧 {tool_name}({args_preview})\033[0m",
|
|
1192
|
+
flush=True,
|
|
1193
|
+
)
|
|
1194
|
+
elif tool_name == "read_file":
|
|
1111
1195
|
print(
|
|
1112
1196
|
f"\033[2m📖 Reading: {tool_args.get('path', '')}\033[0m",
|
|
1113
1197
|
flush=True,
|
|
@@ -1250,7 +1334,7 @@ class PatchPalAgent:
|
|
|
1250
1334
|
tool_result = tool_func(**filtered_args)
|
|
1251
1335
|
except Exception as e:
|
|
1252
1336
|
tool_result = f"Error executing {tool_name}: {e}"
|
|
1253
|
-
print(f"\033[1;31m✗ {
|
|
1337
|
+
print(f"\033[1;31m✗ {tool_name}: {e}\033[0m")
|
|
1254
1338
|
|
|
1255
1339
|
# Add tool result to messages
|
|
1256
1340
|
self.messages.append(
|
|
@@ -1299,18 +1383,33 @@ class PatchPalAgent:
|
|
|
1299
1383
|
)
|
|
1300
1384
|
|
|
1301
1385
|
|
|
1302
|
-
def create_agent(model_id: str = "anthropic/claude-sonnet-4-5") -> PatchPalAgent:
|
|
1386
|
+
def create_agent(model_id: str = "anthropic/claude-sonnet-4-5", custom_tools=None) -> PatchPalAgent:
|
|
1303
1387
|
"""Create and return a PatchPal agent.
|
|
1304
1388
|
|
|
1305
1389
|
Args:
|
|
1306
1390
|
model_id: LiteLLM model identifier (default: anthropic/claude-sonnet-4-5)
|
|
1391
|
+
custom_tools: Optional list of Python functions to use as custom tools.
|
|
1392
|
+
Each function should have type hints and a docstring.
|
|
1307
1393
|
|
|
1308
1394
|
Returns:
|
|
1309
1395
|
A configured PatchPalAgent instance
|
|
1396
|
+
|
|
1397
|
+
Example:
|
|
1398
|
+
def calculator(x: int, y: int) -> str:
|
|
1399
|
+
'''Add two numbers.
|
|
1400
|
+
|
|
1401
|
+
Args:
|
|
1402
|
+
x: First number
|
|
1403
|
+
y: Second number
|
|
1404
|
+
'''
|
|
1405
|
+
return str(x + y)
|
|
1406
|
+
|
|
1407
|
+
agent = create_agent(custom_tools=[calculator])
|
|
1408
|
+
response = agent.run("What's 5 + 3?")
|
|
1310
1409
|
"""
|
|
1311
1410
|
# Reset session todos for new session
|
|
1312
1411
|
from patchpal.tools import reset_session_todos
|
|
1313
1412
|
|
|
1314
1413
|
reset_session_todos()
|
|
1315
1414
|
|
|
1316
|
-
return PatchPalAgent(model_id=model_id)
|
|
1415
|
+
return PatchPalAgent(model_id=model_id, custom_tools=custom_tools)
|
|
@@ -105,14 +105,19 @@ class PermissionManager:
|
|
|
105
105
|
self.session_grants[tool_name] = True
|
|
106
106
|
|
|
107
107
|
def request_permission(
|
|
108
|
-
self,
|
|
108
|
+
self,
|
|
109
|
+
tool_name: str,
|
|
110
|
+
description: str,
|
|
111
|
+
pattern: Optional[str] = None,
|
|
112
|
+
context: Optional[str] = None,
|
|
109
113
|
) -> bool:
|
|
110
114
|
"""Request permission from user to execute a tool.
|
|
111
115
|
|
|
112
116
|
Args:
|
|
113
117
|
tool_name: Name of the tool (e.g., 'run_shell', 'apply_patch')
|
|
114
118
|
description: Human-readable description of what will be executed
|
|
115
|
-
pattern: Optional pattern for matching (e.g., 'pytest' for pytest commands)
|
|
119
|
+
pattern: Optional pattern for matching (e.g., 'pytest' for pytest commands, 'python:/tmp' for python in /tmp)
|
|
120
|
+
context: Optional context string for display (e.g., working directory)
|
|
116
121
|
|
|
117
122
|
Returns:
|
|
118
123
|
True if permission granted, False otherwise
|
|
@@ -135,10 +140,43 @@ class PermissionManager:
|
|
|
135
140
|
sys.stderr.write("-" * 80 + "\n")
|
|
136
141
|
|
|
137
142
|
# Get user input
|
|
143
|
+
# Get the actual repository root for display (match Claude Code's UX)
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
repo_root = Path(".").resolve()
|
|
147
|
+
|
|
138
148
|
sys.stderr.write("\nDo you want to proceed?\n")
|
|
139
149
|
sys.stderr.write(" 1. Yes\n")
|
|
140
150
|
if pattern:
|
|
141
|
-
|
|
151
|
+
# For file operations, pattern is the directory (e.g., "tmp/")
|
|
152
|
+
# For shell commands, pattern is the command name (e.g., "python")
|
|
153
|
+
if tool_name in ("edit_file", "apply_patch"):
|
|
154
|
+
# File operation - show directory context
|
|
155
|
+
if pattern.endswith("/"):
|
|
156
|
+
# Outside repo - directory pattern like "tmp/"
|
|
157
|
+
sys.stderr.write(
|
|
158
|
+
f" 2. Yes, and don't ask again this session for edits in {pattern}\n"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
# Inside repo - file path pattern
|
|
162
|
+
sys.stderr.write(
|
|
163
|
+
f" 2. Yes, and don't ask again this session for edits to {pattern}\n"
|
|
164
|
+
)
|
|
165
|
+
elif tool_name == "run_shell":
|
|
166
|
+
# Shell command - show working directory context
|
|
167
|
+
# Extract command name from pattern (could be "python" or "python@/tmp")
|
|
168
|
+
# Using @ separator for cross-platform compatibility (: conflicts with Windows paths)
|
|
169
|
+
command_name = pattern.split("@")[0] if "@" in pattern else pattern
|
|
170
|
+
|
|
171
|
+
# Use context (working_dir) if provided, otherwise use repo_root
|
|
172
|
+
display_dir = context if context else str(repo_root)
|
|
173
|
+
|
|
174
|
+
sys.stderr.write(
|
|
175
|
+
f" 2. Yes, and don't ask again this session for '{command_name}' commands in {display_dir}\n"
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
# Other tools
|
|
179
|
+
sys.stderr.write(f" 2. Yes, and don't ask again this session for '{pattern}'\n")
|
|
142
180
|
else:
|
|
143
181
|
sys.stderr.write(f" 2. Yes, and don't ask again this session for {tool_name}\n")
|
|
144
182
|
sys.stderr.write(" 3. No, and tell me what to do differently\n")
|