code-puppy 0.0.59__py3-none-any.whl → 0.0.61__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 +15 -4
- code_puppy/agent_prompts.py +2 -1
- code_puppy/command_line/meta_command_handler.py +17 -12
- code_puppy/command_line/prompt_toolkit_completion.py +43 -36
- code_puppy/model_factory.py +0 -108
- code_puppy/models.json +4 -31
- code_puppy/tools/command_runner.py +7 -2
- code_puppy/tools/common.py +51 -0
- code_puppy/tools/file_modifications.py +137 -130
- code_puppy/tools/file_operations.py +19 -28
- code_puppy/tools/ts_code_map.py +393 -0
- {code_puppy-0.0.59.data → code_puppy-0.0.61.data}/data/code_puppy/models.json +4 -31
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/METADATA +3 -1
- code_puppy-0.0.61.dist-info/RECORD +28 -0
- code_puppy/tools/code_map.py +0 -92
- code_puppy-0.0.59.dist-info/RECORD +0 -28
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.59.dist-info → code_puppy-0.0.61.dist-info}/licenses/LICENSE +0 -0
code_puppy/agent.py
CHANGED
|
@@ -18,12 +18,23 @@ from code_puppy.tools.common import console
|
|
|
18
18
|
|
|
19
19
|
MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Puppy rules loader
|
|
22
22
|
PUPPY_RULES_PATH = Path(".puppy_rules")
|
|
23
23
|
PUPPY_RULES = None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_puppy_rules(path=None):
|
|
27
|
+
global PUPPY_RULES
|
|
28
|
+
rules_path = Path(path) if path else PUPPY_RULES_PATH
|
|
29
|
+
if rules_path.exists():
|
|
30
|
+
with open(rules_path, "r") as f:
|
|
31
|
+
PUPPY_RULES = f.read()
|
|
32
|
+
else:
|
|
33
|
+
PUPPY_RULES = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Load at import
|
|
37
|
+
load_puppy_rules()
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
class AgentResponse(pydantic.BaseModel):
|
code_puppy/agent_prompts.py
CHANGED
|
@@ -30,6 +30,7 @@ File Operations:
|
|
|
30
30
|
- edit_file(path, diff): Use this single tool to create new files, overwrite entire files, perform targeted replacements, or delete snippets depending on the JSON/raw payload provided.
|
|
31
31
|
- delete_file(file_path): Use this to remove files when needed
|
|
32
32
|
- grep(search_string, directory="."): Use this to recursively search for a string across files starting from the specified directory, capping results at 200 matches.
|
|
33
|
+
- code_map(directory="."): Use this to generate a code map for the specified directory.
|
|
33
34
|
|
|
34
35
|
Tool Usage Instructions:
|
|
35
36
|
|
|
@@ -49,7 +50,7 @@ Example (create):
|
|
|
49
50
|
edit_file("src/example.py", "print('hello')\n")
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
Example (replacement):
|
|
53
|
+
Example (replacement): -- YOU SHOULD PREFER THIS AS THE PRIMARY WAY TO EDIT FILES.
|
|
53
54
|
```json
|
|
54
55
|
edit_file(
|
|
55
56
|
"src/example.py",
|
|
@@ -6,6 +6,7 @@ from code_puppy.command_line.model_picker_completion import (
|
|
|
6
6
|
load_model_names,
|
|
7
7
|
update_model_in_input,
|
|
8
8
|
)
|
|
9
|
+
from code_puppy.config import get_config_keys
|
|
9
10
|
from code_puppy.command_line.utils import make_directory_table
|
|
10
11
|
|
|
11
12
|
META_COMMANDS_HELP = """
|
|
@@ -20,9 +21,15 @@ META_COMMANDS_HELP = """
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def handle_meta_command(command: str, console: Console) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Handle meta/config commands prefixed with '~'.
|
|
26
|
+
Returns True if the command was handled (even if just an error/help), False if not.
|
|
27
|
+
"""
|
|
28
|
+
command = command.strip()
|
|
29
|
+
|
|
23
30
|
# ~codemap (code structure visualization)
|
|
24
31
|
if command.startswith("~codemap"):
|
|
25
|
-
from code_puppy.tools.
|
|
32
|
+
from code_puppy.tools.ts_code_map import make_code_map
|
|
26
33
|
|
|
27
34
|
tokens = command.split()
|
|
28
35
|
if len(tokens) > 1:
|
|
@@ -30,16 +37,11 @@ def handle_meta_command(command: str, console: Console) -> bool:
|
|
|
30
37
|
else:
|
|
31
38
|
target_dir = os.getcwd()
|
|
32
39
|
try:
|
|
33
|
-
|
|
34
|
-
console.print(tree)
|
|
40
|
+
make_code_map(target_dir, ignore_tests=True)
|
|
35
41
|
except Exception as e:
|
|
36
42
|
console.print(f"[red]Error generating code map:[/red] {e}")
|
|
37
43
|
return True
|
|
38
|
-
|
|
39
|
-
Handle meta/config commands prefixed with '~'.
|
|
40
|
-
Returns True if the command was handled (even if just an error/help), False if not.
|
|
41
|
-
"""
|
|
42
|
-
command = command.strip()
|
|
44
|
+
|
|
43
45
|
if command.startswith("~cd"):
|
|
44
46
|
tokens = command.split()
|
|
45
47
|
if len(tokens) == 1:
|
|
@@ -83,7 +85,7 @@ def handle_meta_command(command: str, console: Console) -> bool:
|
|
|
83
85
|
|
|
84
86
|
if command.startswith("~set"):
|
|
85
87
|
# Syntax: ~set KEY=VALUE or ~set KEY VALUE
|
|
86
|
-
from code_puppy.config import
|
|
88
|
+
from code_puppy.config import set_config_value
|
|
87
89
|
|
|
88
90
|
tokens = command.split(None, 2)
|
|
89
91
|
argstr = command[len("~set") :].strip()
|
|
@@ -100,8 +102,9 @@ def handle_meta_command(command: str, console: Console) -> bool:
|
|
|
100
102
|
key = tokens[1]
|
|
101
103
|
value = ""
|
|
102
104
|
else:
|
|
103
|
-
console.print(
|
|
104
|
-
|
|
105
|
+
console.print(
|
|
106
|
+
f"[yellow]Usage:[/yellow] ~set KEY=VALUE or ~set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
|
|
107
|
+
)
|
|
105
108
|
return True
|
|
106
109
|
if key:
|
|
107
110
|
set_config_value(key, value)
|
|
@@ -116,9 +119,11 @@ def handle_meta_command(command: str, console: Console) -> bool:
|
|
|
116
119
|
# Try setting model and show confirmation
|
|
117
120
|
new_input = update_model_in_input(command)
|
|
118
121
|
if new_input is not None:
|
|
122
|
+
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
119
123
|
from code_puppy.agent import get_code_generation_agent
|
|
120
124
|
|
|
121
125
|
model = get_active_model()
|
|
126
|
+
# Make sure this is called for the test
|
|
122
127
|
get_code_generation_agent(force_reload=True)
|
|
123
128
|
console.print(
|
|
124
129
|
f"[bold green]Active model set and loaded:[/bold green] [cyan]{model}[/cyan]"
|
|
@@ -126,8 +131,8 @@ def handle_meta_command(command: str, console: Console) -> bool:
|
|
|
126
131
|
return True
|
|
127
132
|
# If no model matched, show available models
|
|
128
133
|
model_names = load_model_names()
|
|
134
|
+
console.print("[yellow]Usage:[/yellow] ~m <model-name>")
|
|
129
135
|
console.print(f"[yellow]Available models:[/yellow] {', '.join(model_names)}")
|
|
130
|
-
console.print("[yellow]Usage:[/yellow] ~m <model_name>")
|
|
131
136
|
return True
|
|
132
137
|
if command in ("~help", "~h"):
|
|
133
138
|
console.print(META_COMMANDS_HELP)
|
|
@@ -33,53 +33,60 @@ class SetCompleter(Completer):
|
|
|
33
33
|
self.trigger = trigger
|
|
34
34
|
|
|
35
35
|
def get_completions(self, document, complete_event):
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
text_before_cursor = document.text_before_cursor
|
|
37
|
+
stripped_text_for_trigger_check = text_before_cursor.lstrip()
|
|
38
|
+
|
|
39
|
+
if not stripped_text_for_trigger_check.startswith(self.trigger):
|
|
38
40
|
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
# Determine the part of the text that is relevant for this completer
|
|
43
|
+
# This handles cases like " ~set foo" where the trigger isn't at the start of the string
|
|
44
|
+
actual_trigger_pos = text_before_cursor.find(self.trigger)
|
|
45
|
+
effective_input = text_before_cursor[
|
|
46
|
+
actual_trigger_pos:
|
|
47
|
+
] # e.g., "~set keypart" or "~set " or "~set"
|
|
48
|
+
|
|
49
|
+
tokens = effective_input.split()
|
|
50
|
+
|
|
51
|
+
# Case 1: Input is exactly the trigger (e.g., "~set") and nothing more (not even a trailing space on effective_input).
|
|
52
|
+
# Suggest adding a space.
|
|
53
|
+
if (
|
|
54
|
+
len(tokens) == 1
|
|
55
|
+
and tokens[0] == self.trigger
|
|
56
|
+
and not effective_input.endswith(" ")
|
|
57
|
+
):
|
|
41
58
|
yield Completion(
|
|
42
|
-
self.trigger + " ",
|
|
43
|
-
start_position=-len(
|
|
44
|
-
display=
|
|
45
|
-
display_meta="set config",
|
|
59
|
+
text=self.trigger + " ", # Text to insert
|
|
60
|
+
start_position=-len(tokens[0]), # Replace the trigger itself
|
|
61
|
+
display=self.trigger + " ", # Visual display
|
|
62
|
+
display_meta="set config key",
|
|
46
63
|
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Case 2: Input is trigger + space (e.g., "~set ") or trigger + partial key (e.g., "~set partial")
|
|
67
|
+
base_to_complete = ""
|
|
68
|
+
if len(tokens) > 1: # e.g., ["~set", "partialkey"]
|
|
69
|
+
base_to_complete = tokens[1]
|
|
70
|
+
# If len(tokens) == 1, it implies effective_input was like "~set ", so base_to_complete remains ""
|
|
71
|
+
# This means we list all keys.
|
|
72
|
+
|
|
54
73
|
# --- SPECIAL HANDLING FOR 'model' KEY ---
|
|
55
|
-
if
|
|
74
|
+
if base_to_complete == "model":
|
|
56
75
|
# Don't return any completions -- let ModelNameCompleter handle it
|
|
57
76
|
return
|
|
58
77
|
for key in get_config_keys():
|
|
59
78
|
if key == "model":
|
|
60
79
|
continue # exclude 'model' from regular ~set completions
|
|
61
|
-
if key.startswith(
|
|
80
|
+
if key.startswith(base_to_complete):
|
|
62
81
|
prev_value = get_value(key)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
insert_text = (
|
|
67
|
-
f"{prefix}{key} = {prev_value}"
|
|
68
|
-
if prev_value is not None
|
|
69
|
-
else f"{prefix}{key} = "
|
|
70
|
-
)
|
|
71
|
-
sp = -len(text)
|
|
72
|
-
else:
|
|
73
|
-
insert_text = (
|
|
74
|
-
f"{key} = {prev_value}"
|
|
75
|
-
if prev_value is not None
|
|
76
|
-
else f"{key} = "
|
|
77
|
-
)
|
|
78
|
-
sp = -len(base)
|
|
79
|
-
# Make it obvious the value part is from before
|
|
82
|
+
value_part = f" = {prev_value}" if prev_value is not None else " = "
|
|
83
|
+
completion_text = f"{key}{value_part}"
|
|
84
|
+
|
|
80
85
|
yield Completion(
|
|
81
|
-
|
|
82
|
-
start_position
|
|
86
|
+
completion_text,
|
|
87
|
+
start_position=-len(
|
|
88
|
+
base_to_complete
|
|
89
|
+
), # Correctly replace only the typed part of the key
|
|
83
90
|
display_meta=f"puppy.cfg key (was: {prev_value})"
|
|
84
91
|
if prev_value is not None
|
|
85
92
|
else "puppy.cfg key",
|
code_puppy/model_factory.py
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import json
|
|
3
2
|
import os
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
from collections import deque
|
|
7
3
|
from typing import Any, Dict
|
|
8
4
|
|
|
9
5
|
import httpx
|
|
10
6
|
from anthropic import AsyncAnthropic
|
|
11
|
-
from httpx import Response
|
|
12
7
|
from openai import AsyncAzureOpenAI # For Azure OpenAI client
|
|
13
8
|
from pydantic_ai.models.anthropic import AnthropicModel
|
|
14
9
|
from pydantic_ai.models.gemini import GeminiModel
|
|
@@ -27,98 +22,6 @@ from pydantic_ai.providers.openai import OpenAIProvider
|
|
|
27
22
|
# Example: "X-Api-Key": "$OPENAI_API_KEY" will use the value from os.environ.get("OPENAI_API_KEY")
|
|
28
23
|
|
|
29
24
|
|
|
30
|
-
def make_client(
|
|
31
|
-
max_requests_per_minute: int = 10, max_retries: int = 3, retry_base_delay: int = 10
|
|
32
|
-
) -> httpx.AsyncClient:
|
|
33
|
-
# Create a rate limiter using a token bucket approach
|
|
34
|
-
class RateLimiter:
|
|
35
|
-
def __init__(self, max_requests_per_minute):
|
|
36
|
-
self.max_requests_per_minute = max_requests_per_minute
|
|
37
|
-
self.interval = (
|
|
38
|
-
60.0 / max_requests_per_minute
|
|
39
|
-
) # Time between requests in seconds
|
|
40
|
-
self.request_times = deque(maxlen=max_requests_per_minute)
|
|
41
|
-
self.lock = threading.Lock()
|
|
42
|
-
|
|
43
|
-
async def acquire(self):
|
|
44
|
-
"""Wait until a request can be made according to the rate limit."""
|
|
45
|
-
while True:
|
|
46
|
-
with self.lock:
|
|
47
|
-
now = time.time()
|
|
48
|
-
|
|
49
|
-
# Remove timestamps older than 1 minute
|
|
50
|
-
while self.request_times and now - self.request_times[0] > 60:
|
|
51
|
-
self.request_times.popleft()
|
|
52
|
-
|
|
53
|
-
# If we haven't reached the limit, add the timestamp and proceed
|
|
54
|
-
if len(self.request_times) < self.max_requests_per_minute:
|
|
55
|
-
self.request_times.append(now)
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
# Otherwise, calculate the wait time until we can make another request
|
|
59
|
-
oldest = self.request_times[0]
|
|
60
|
-
wait_time = max(0, oldest + 60 - now)
|
|
61
|
-
|
|
62
|
-
if wait_time > 0:
|
|
63
|
-
print(
|
|
64
|
-
f"Rate limit would be exceeded. Waiting {wait_time:.2f} seconds before sending request."
|
|
65
|
-
)
|
|
66
|
-
await asyncio.sleep(wait_time)
|
|
67
|
-
else:
|
|
68
|
-
# Try again immediately
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
# Create the rate limiter instance
|
|
72
|
-
rate_limiter = RateLimiter(max_requests_per_minute)
|
|
73
|
-
|
|
74
|
-
def should_retry(response: Response) -> bool:
|
|
75
|
-
return response.status_code == 429 or (500 <= response.status_code < 600)
|
|
76
|
-
|
|
77
|
-
async def request_hook(request):
|
|
78
|
-
# Wait until we can make a request according to our rate limit
|
|
79
|
-
await rate_limiter.acquire()
|
|
80
|
-
return request
|
|
81
|
-
|
|
82
|
-
async def response_hook(response: Response) -> Response:
|
|
83
|
-
retries = getattr(response.request, "_retries", 0)
|
|
84
|
-
|
|
85
|
-
if should_retry(response) and retries < max_retries:
|
|
86
|
-
setattr(response.request, "_retries", retries + 1)
|
|
87
|
-
|
|
88
|
-
delay = retry_base_delay * (2**retries)
|
|
89
|
-
|
|
90
|
-
if response.status_code == 429:
|
|
91
|
-
print(
|
|
92
|
-
f"Rate limit exceeded. Retrying in {delay:.2f} seconds (attempt {retries + 1}/{max_retries})"
|
|
93
|
-
)
|
|
94
|
-
else:
|
|
95
|
-
print(
|
|
96
|
-
f"Server error {response.status_code}. Retrying in {delay:.2f} seconds (attempt {retries + 1}/{max_retries})"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
await asyncio.sleep(delay)
|
|
100
|
-
|
|
101
|
-
new_request = response.request.copy()
|
|
102
|
-
async with httpx.AsyncClient() as client:
|
|
103
|
-
# Apply rate limiting to the retry request as well
|
|
104
|
-
await rate_limiter.acquire()
|
|
105
|
-
new_response = await client.request(
|
|
106
|
-
new_request.method,
|
|
107
|
-
str(new_request.url),
|
|
108
|
-
headers=new_request.headers,
|
|
109
|
-
content=new_request.content,
|
|
110
|
-
params=dict(new_request.url.params),
|
|
111
|
-
)
|
|
112
|
-
return new_response
|
|
113
|
-
return response
|
|
114
|
-
|
|
115
|
-
# Setup both request and response hooks
|
|
116
|
-
event_hooks = {"request": [request_hook], "response": [response_hook]}
|
|
117
|
-
|
|
118
|
-
client = httpx.AsyncClient(event_hooks=event_hooks)
|
|
119
|
-
return client
|
|
120
|
-
|
|
121
|
-
|
|
122
25
|
def get_custom_config(model_config):
|
|
123
26
|
custom_config = model_config.get("custom_endpoint", {})
|
|
124
27
|
if not custom_config:
|
|
@@ -167,17 +70,6 @@ class ModelFactory:
|
|
|
167
70
|
|
|
168
71
|
model_type = model_config.get("type")
|
|
169
72
|
|
|
170
|
-
# Common configuration for rate limiting and retries
|
|
171
|
-
max_requests_per_minute = model_config.get("max_requests_per_minute", 100)
|
|
172
|
-
max_retries = model_config.get("max_retries", 3)
|
|
173
|
-
retry_base_delay = model_config.get("retry_base_delay", 1.0)
|
|
174
|
-
|
|
175
|
-
client = make_client(
|
|
176
|
-
max_requests_per_minute=max_requests_per_minute,
|
|
177
|
-
max_retries=max_retries,
|
|
178
|
-
retry_base_delay=retry_base_delay,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
73
|
if model_type == "gemini":
|
|
182
74
|
provider = GoogleGLAProvider(api_key=os.environ.get("GEMINI_API_KEY", ""))
|
|
183
75
|
|
code_puppy/models.json
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"gemini-2.5-flash-preview-05-20": {
|
|
3
3
|
"type": "gemini",
|
|
4
|
-
"name": "gemini-2.5-flash-preview-05-20"
|
|
5
|
-
"max_requests_per_minute": 10,
|
|
6
|
-
"max_retries": 3,
|
|
7
|
-
"retry_base_delay": 10
|
|
4
|
+
"name": "gemini-2.5-flash-preview-05-20"
|
|
8
5
|
},
|
|
9
6
|
"gpt-4.1": {
|
|
10
7
|
"type": "openai",
|
|
11
|
-
"name": "gpt-4.1"
|
|
12
|
-
"max_requests_per_minute": 100,
|
|
13
|
-
"max_retries": 3,
|
|
14
|
-
"retry_base_delay": 10
|
|
8
|
+
"name": "gpt-4.1"
|
|
15
9
|
},
|
|
16
10
|
"gpt-4.1-mini": {
|
|
17
11
|
"type": "openai",
|
|
18
|
-
"name": "gpt-4.1-mini"
|
|
19
|
-
"max_requests_per_minute": 100,
|
|
20
|
-
"max_retries": 3,
|
|
21
|
-
"retry_base_delay": 10
|
|
12
|
+
"name": "gpt-4.1-mini"
|
|
22
13
|
},
|
|
23
14
|
"gpt-4.1-nano": {
|
|
24
15
|
"type": "openai",
|
|
25
|
-
"name": "gpt-4.1-nano"
|
|
26
|
-
"max_requests_per_minute": 100,
|
|
27
|
-
"max_retries": 3,
|
|
28
|
-
"retry_base_delay": 10
|
|
16
|
+
"name": "gpt-4.1-nano"
|
|
29
17
|
},
|
|
30
18
|
"gpt-4.1-custom": {
|
|
31
19
|
"type": "custom_openai",
|
|
32
20
|
"name": "gpt-4.1-custom",
|
|
33
|
-
"max_requests_per_minute": 100,
|
|
34
|
-
"max_retries": 3,
|
|
35
|
-
"retry_base_delay": 10,
|
|
36
21
|
"custom_endpoint": {
|
|
37
22
|
"url": "https://my.cute.endpoint:8080",
|
|
38
23
|
"headers": {
|
|
@@ -44,9 +29,6 @@
|
|
|
44
29
|
"ollama-llama3.3": {
|
|
45
30
|
"type": "custom_openai",
|
|
46
31
|
"name": "llama3.3",
|
|
47
|
-
"max_requests_per_minute": 100,
|
|
48
|
-
"max_retries": 3,
|
|
49
|
-
"retry_base_delay": 5,
|
|
50
32
|
"custom_endpoint": {
|
|
51
33
|
"url": "http://localhost:11434/v1"
|
|
52
34
|
}
|
|
@@ -54,9 +36,6 @@
|
|
|
54
36
|
"meta-llama/Llama-3.3-70B-Instruct-Turbo": {
|
|
55
37
|
"type": "custom_openai",
|
|
56
38
|
"name": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
57
|
-
"max_requests_per_minute": 100,
|
|
58
|
-
"max_retries": 3,
|
|
59
|
-
"retry_base_delay": 5,
|
|
60
39
|
"custom_endpoint": {
|
|
61
40
|
"url": "https://api.together.xyz/v1",
|
|
62
41
|
"api_key": "$TOGETHER_API_KEY"
|
|
@@ -65,9 +44,6 @@
|
|
|
65
44
|
"grok-3-mini-fast": {
|
|
66
45
|
"type": "custom_openai",
|
|
67
46
|
"name": "grok-3-mini-fast",
|
|
68
|
-
"max_requests_per_minute": 100,
|
|
69
|
-
"max_retries": 3,
|
|
70
|
-
"retry_base_delay": 5,
|
|
71
47
|
"custom_endpoint": {
|
|
72
48
|
"url": "https://api.x.ai/v1",
|
|
73
49
|
"api_key": "$XAI_API_KEY"
|
|
@@ -76,9 +52,6 @@
|
|
|
76
52
|
"azure-gpt-4.1": {
|
|
77
53
|
"type": "azure_openai",
|
|
78
54
|
"name": "gpt-4.1",
|
|
79
|
-
"max_requests_per_minute": 100,
|
|
80
|
-
"max_retries": 3,
|
|
81
|
-
"retry_base_delay": 5,
|
|
82
55
|
"api_version": "2024-12-01-preview",
|
|
83
56
|
"api_key": "$AZURE_OPENAI_API_KEY",
|
|
84
57
|
"azure_endpoint": "$AZURE_OPENAI_ENDPOINT"
|
|
@@ -136,12 +136,17 @@ def run_shell_command(
|
|
|
136
136
|
except Exception as e:
|
|
137
137
|
console.print_exception(show_locals=True)
|
|
138
138
|
console.print("[dim]" + "-" * 60 + "[/dim]\n")
|
|
139
|
+
# Ensure stdout and stderr are always defined
|
|
140
|
+
if "stdout" not in locals():
|
|
141
|
+
stdout = None
|
|
142
|
+
if "stderr" not in locals():
|
|
143
|
+
stderr = None
|
|
139
144
|
return {
|
|
140
145
|
"success": False,
|
|
141
146
|
"command": command,
|
|
142
147
|
"error": f"Error executing command: {str(e)}",
|
|
143
|
-
"stdout": stdout[-1000:],
|
|
144
|
-
"stderr": stderr[-1000:],
|
|
148
|
+
"stdout": stdout[-1000:] if stdout else None,
|
|
149
|
+
"stderr": stderr[-1000:] if stderr else None,
|
|
145
150
|
"exit_code": -1,
|
|
146
151
|
"timeout": False,
|
|
147
152
|
}
|
code_puppy/tools/common.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import fnmatch
|
|
2
3
|
|
|
3
4
|
from typing import Optional, Tuple
|
|
4
5
|
|
|
@@ -8,6 +9,56 @@ from rich.console import Console
|
|
|
8
9
|
NO_COLOR = bool(int(os.environ.get("CODE_PUPPY_NO_COLOR", "0")))
|
|
9
10
|
console = Console(no_color=NO_COLOR)
|
|
10
11
|
|
|
12
|
+
|
|
13
|
+
# -------------------
|
|
14
|
+
# Shared ignore patterns/helpers
|
|
15
|
+
# -------------------
|
|
16
|
+
IGNORE_PATTERNS = [
|
|
17
|
+
"**/node_modules/**",
|
|
18
|
+
"**/node_modules/**/*.js",
|
|
19
|
+
"node_modules/**",
|
|
20
|
+
"node_modules",
|
|
21
|
+
"**/.git/**",
|
|
22
|
+
"**/.git",
|
|
23
|
+
".git/**",
|
|
24
|
+
".git",
|
|
25
|
+
"**/__pycache__/**",
|
|
26
|
+
"**/__pycache__",
|
|
27
|
+
"__pycache__/**",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
"**/.DS_Store",
|
|
30
|
+
".DS_Store",
|
|
31
|
+
"**/.env",
|
|
32
|
+
".env",
|
|
33
|
+
"**/.venv/**",
|
|
34
|
+
"**/.venv",
|
|
35
|
+
"**/venv/**",
|
|
36
|
+
"**/venv",
|
|
37
|
+
"**/.idea/**",
|
|
38
|
+
"**/.idea",
|
|
39
|
+
"**/.vscode/**",
|
|
40
|
+
"**/.vscode",
|
|
41
|
+
"**/dist/**",
|
|
42
|
+
"**/dist",
|
|
43
|
+
"**/build/**",
|
|
44
|
+
"**/build",
|
|
45
|
+
"**/*.pyc",
|
|
46
|
+
"**/*.pyo",
|
|
47
|
+
"**/*.pyd",
|
|
48
|
+
"**/*.so",
|
|
49
|
+
"**/*.dll",
|
|
50
|
+
"**/.*",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def should_ignore_path(path: str) -> bool:
|
|
55
|
+
"""Return True if *path* matches any pattern in IGNORE_PATTERNS."""
|
|
56
|
+
for pattern in IGNORE_PATTERNS:
|
|
57
|
+
if fnmatch.fnmatch(path, pattern):
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
11
62
|
JW_THRESHOLD = 0.95
|
|
12
63
|
|
|
13
64
|
|