code-puppy 0.0.94__tar.gz → 0.0.96__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.
- {code_puppy-0.0.94 → code_puppy-0.0.96}/PKG-INFO +1 -1
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/agent.py +4 -1
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/prompt_toolkit_completion.py +2 -6
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/main.py +109 -58
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/message_history_processor.py +10 -7
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/status_display.py +55 -41
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/__init__.py +0 -1
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/command_runner.py +5 -2
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/common.py +2 -1
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/file_operations.py +21 -8
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/token_check.py +7 -2
- {code_puppy-0.0.94 → code_puppy-0.0.96}/pyproject.toml +1 -1
- {code_puppy-0.0.94 → code_puppy-0.0.96}/.gitignore +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/LICENSE +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/README.md +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/agent_prompts.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/meta_command_handler.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/config.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/models.json +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/state_management.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/token_utils.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/version_checker.py +0 -0
|
@@ -19,6 +19,7 @@ from code_puppy.tools.common import console
|
|
|
19
19
|
|
|
20
20
|
MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH", None)
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
def load_puppy_rules():
|
|
23
24
|
global PUPPY_RULES
|
|
24
25
|
puppy_rules_path = Path("AGENT.md")
|
|
@@ -27,9 +28,11 @@ def load_puppy_rules():
|
|
|
27
28
|
puppy_rules = f.read()
|
|
28
29
|
return puppy_rules
|
|
29
30
|
|
|
31
|
+
|
|
30
32
|
# Load at import
|
|
31
33
|
PUPPY_RULES = load_puppy_rules()
|
|
32
34
|
|
|
35
|
+
|
|
33
36
|
class AgentResponse(pydantic.BaseModel):
|
|
34
37
|
"""Represents a response from the agent."""
|
|
35
38
|
|
|
@@ -80,7 +83,7 @@ def reload_code_generation_agent():
|
|
|
80
83
|
output_type=str,
|
|
81
84
|
retries=3,
|
|
82
85
|
history_processors=[message_history_accumulator],
|
|
83
|
-
toolsets=_load_mcp_servers()
|
|
86
|
+
toolsets=_load_mcp_servers(),
|
|
84
87
|
)
|
|
85
88
|
register_all_tools(agent)
|
|
86
89
|
_code_generation_agent = agent
|
{code_puppy-0.0.94 → code_puppy-0.0.96}/code_puppy/command_line/prompt_toolkit_completion.py
RENAMED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
1
|
# ANSI color codes are no longer necessary because prompt_toolkit handles
|
|
6
2
|
# styling via the `Style` class. We keep them here commented-out in case
|
|
7
3
|
# someone needs raw ANSI later, but they are unused in the current code.
|
|
@@ -175,7 +171,7 @@ async def get_input_with_combined_completion(
|
|
|
175
171
|
def _(event):
|
|
176
172
|
event.app.current_buffer.insert_text("\n")
|
|
177
173
|
|
|
178
|
-
@bindings.add(
|
|
174
|
+
@bindings.add("c-c")
|
|
179
175
|
def _(event):
|
|
180
176
|
"""Cancel the current prompt when the user presses the ESC key alone."""
|
|
181
177
|
event.app.exit(exception=KeyboardInterrupt)
|
|
@@ -226,4 +222,4 @@ if __name__ == "__main__":
|
|
|
226
222
|
break
|
|
227
223
|
print("\nGoodbye!")
|
|
228
224
|
|
|
229
|
-
asyncio.run(main())
|
|
225
|
+
asyncio.run(main())
|
|
@@ -28,6 +28,7 @@ from code_puppy.message_history_processor import message_history_processor
|
|
|
28
28
|
# from code_puppy.tools import * # noqa: F403
|
|
29
29
|
import logfire
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
# Define a function to get the secret file path
|
|
32
33
|
def get_secret_file_path():
|
|
33
34
|
hidden_directory = os.path.join(os.path.expanduser("~"), ".agent_secret")
|
|
@@ -39,8 +40,7 @@ def get_secret_file_path():
|
|
|
39
40
|
async def main():
|
|
40
41
|
# Ensure the config directory and puppy.cfg with name info exist (prompt user if needed)
|
|
41
42
|
logfire.configure(
|
|
42
|
-
token="pylf_v1_us_8G5nLznQtHMRsL4hsNG5v3fPWKjyXbysrMgrQ1bV1wRP",
|
|
43
|
-
console=False
|
|
43
|
+
token="pylf_v1_us_8G5nLznQtHMRsL4hsNG5v3fPWKjyXbysrMgrQ1bV1wRP", console=False
|
|
44
44
|
)
|
|
45
45
|
logfire.instrument_pydantic_ai()
|
|
46
46
|
ensure_config_exists()
|
|
@@ -201,150 +201,196 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
201
201
|
try:
|
|
202
202
|
prettier_code_blocks()
|
|
203
203
|
local_cancelled = False
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
# Initialize status display for tokens per second and loading messages
|
|
206
206
|
status_display = StatusDisplay(console)
|
|
207
|
-
|
|
207
|
+
|
|
208
208
|
# Print a message indicating we're about to start processing
|
|
209
209
|
console.print("\nStarting task processing...")
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
async def track_tokens_from_messages():
|
|
212
212
|
"""
|
|
213
213
|
Track real token counts from message history.
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
This async function runs in the background and periodically checks
|
|
216
216
|
the message history for new tokens. When new tokens are detected,
|
|
217
217
|
it updates the StatusDisplay with the incremental count to calculate
|
|
218
218
|
an accurate tokens-per-second rate.
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
It also looks for SSE stream time_info data to get precise token rate
|
|
221
221
|
calculations using the formula: completion_tokens * 1 / completion_time
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
The function continues running until status_display.is_active becomes False.
|
|
224
224
|
"""
|
|
225
|
-
from code_puppy.message_history_processor import
|
|
225
|
+
from code_puppy.message_history_processor import (
|
|
226
|
+
estimate_tokens_for_message,
|
|
227
|
+
)
|
|
226
228
|
import json
|
|
227
229
|
import re
|
|
228
|
-
|
|
230
|
+
|
|
229
231
|
last_token_total = 0
|
|
230
232
|
last_sse_data = None
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
while status_display.is_active:
|
|
233
235
|
# Get real token count from message history
|
|
234
236
|
messages = get_message_history()
|
|
235
237
|
if messages:
|
|
236
238
|
# Calculate total tokens across all messages
|
|
237
|
-
current_token_total = sum(
|
|
238
|
-
|
|
239
|
+
current_token_total = sum(
|
|
240
|
+
estimate_tokens_for_message(msg) for msg in messages
|
|
241
|
+
)
|
|
242
|
+
|
|
239
243
|
# If tokens increased, update the display with the incremental count
|
|
240
244
|
if current_token_total > last_token_total:
|
|
241
|
-
status_display.update_token_count(
|
|
245
|
+
status_display.update_token_count(
|
|
246
|
+
current_token_total - last_token_total
|
|
247
|
+
)
|
|
242
248
|
last_token_total = current_token_total
|
|
243
|
-
|
|
249
|
+
|
|
244
250
|
# Try to find SSE stream data in assistant messages
|
|
245
251
|
for msg in messages:
|
|
246
252
|
# Handle different message types (dict or ModelMessage objects)
|
|
247
|
-
if hasattr(msg,
|
|
253
|
+
if hasattr(msg, "role") and msg.role == "assistant":
|
|
248
254
|
# ModelMessage object with role attribute
|
|
249
|
-
content =
|
|
250
|
-
|
|
255
|
+
content = (
|
|
256
|
+
msg.content if hasattr(msg, "content") else ""
|
|
257
|
+
)
|
|
258
|
+
elif (
|
|
259
|
+
isinstance(msg, dict)
|
|
260
|
+
and msg.get("role") == "assistant"
|
|
261
|
+
):
|
|
251
262
|
# Dictionary with 'role' key
|
|
252
|
-
content = msg.get(
|
|
263
|
+
content = msg.get("content", "")
|
|
253
264
|
# Support for ModelRequest/ModelResponse objects
|
|
254
|
-
elif
|
|
265
|
+
elif (
|
|
266
|
+
hasattr(msg, "message")
|
|
267
|
+
and hasattr(msg.message, "role")
|
|
268
|
+
and msg.message.role == "assistant"
|
|
269
|
+
):
|
|
255
270
|
# Access content through the message attribute
|
|
256
|
-
content =
|
|
271
|
+
content = (
|
|
272
|
+
msg.message.content
|
|
273
|
+
if hasattr(msg.message, "content")
|
|
274
|
+
else ""
|
|
275
|
+
)
|
|
257
276
|
else:
|
|
258
277
|
# Skip if not an assistant message or unrecognized format
|
|
259
278
|
continue
|
|
260
|
-
|
|
279
|
+
|
|
261
280
|
# Convert content to string if it's not already
|
|
262
281
|
if not isinstance(content, str):
|
|
263
282
|
try:
|
|
264
283
|
content = str(content)
|
|
265
|
-
except:
|
|
284
|
+
except Exception:
|
|
266
285
|
continue
|
|
267
|
-
|
|
286
|
+
|
|
268
287
|
# Look for SSE usage data pattern in the message content
|
|
269
|
-
sse_matches = re.findall(
|
|
288
|
+
sse_matches = re.findall(
|
|
289
|
+
r'\{\s*"usage".*?"time_info".*?\}',
|
|
290
|
+
content,
|
|
291
|
+
re.DOTALL,
|
|
292
|
+
)
|
|
270
293
|
for match in sse_matches:
|
|
271
294
|
try:
|
|
272
295
|
# Parse the JSON data
|
|
273
296
|
sse_data = json.loads(match)
|
|
274
|
-
if
|
|
297
|
+
if (
|
|
298
|
+
sse_data != last_sse_data
|
|
299
|
+
): # Only process new data
|
|
275
300
|
# Check if we have time_info and completion_tokens
|
|
276
|
-
if
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
if (
|
|
302
|
+
"time_info" in sse_data
|
|
303
|
+
and "completion_time"
|
|
304
|
+
in sse_data["time_info"]
|
|
305
|
+
and "usage" in sse_data
|
|
306
|
+
and "completion_tokens"
|
|
307
|
+
in sse_data["usage"]
|
|
308
|
+
):
|
|
309
|
+
completion_time = float(
|
|
310
|
+
sse_data["time_info"][
|
|
311
|
+
"completion_time"
|
|
312
|
+
]
|
|
313
|
+
)
|
|
314
|
+
completion_tokens = int(
|
|
315
|
+
sse_data["usage"][
|
|
316
|
+
"completion_tokens"
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
|
|
281
320
|
# Update rate using the accurate SSE data
|
|
282
|
-
if
|
|
283
|
-
|
|
321
|
+
if (
|
|
322
|
+
completion_time > 0
|
|
323
|
+
and completion_tokens > 0
|
|
324
|
+
):
|
|
325
|
+
status_display.update_rate_from_sse(
|
|
326
|
+
completion_tokens,
|
|
327
|
+
completion_time,
|
|
328
|
+
)
|
|
284
329
|
last_sse_data = sse_data
|
|
285
330
|
except (json.JSONDecodeError, KeyError, ValueError):
|
|
286
331
|
# Ignore parsing errors and continue
|
|
287
332
|
pass
|
|
288
|
-
|
|
333
|
+
|
|
289
334
|
# Small sleep interval for responsive updates without excessive CPU usage
|
|
290
335
|
await asyncio.sleep(0.1)
|
|
291
|
-
|
|
336
|
+
|
|
292
337
|
async def wrap_agent_run(original_run, *args, **kwargs):
|
|
293
338
|
"""
|
|
294
339
|
Wraps the agent's run method to enable token tracking.
|
|
295
|
-
|
|
340
|
+
|
|
296
341
|
This wrapper preserves the original functionality while allowing
|
|
297
342
|
us to track tokens as they are generated by the model. No additional
|
|
298
343
|
logic is needed here since the token tracking happens in a separate task.
|
|
299
|
-
|
|
344
|
+
|
|
300
345
|
Args:
|
|
301
346
|
original_run: The original agent.run method
|
|
302
347
|
*args, **kwargs: Arguments to pass to the original run method
|
|
303
|
-
|
|
348
|
+
|
|
304
349
|
Returns:
|
|
305
350
|
The result from the original run method
|
|
306
351
|
"""
|
|
307
352
|
result = await original_run(*args, **kwargs)
|
|
308
353
|
return result
|
|
309
|
-
|
|
354
|
+
|
|
310
355
|
async def run_agent_task():
|
|
311
356
|
"""
|
|
312
357
|
Main task runner for the agent with token tracking.
|
|
313
|
-
|
|
314
|
-
This function:
|
|
358
|
+
|
|
359
|
+
This function:
|
|
315
360
|
1. Sets up the agent with token tracking
|
|
316
361
|
2. Starts the status display showing token rate
|
|
317
362
|
3. Runs the agent with the user's task
|
|
318
363
|
4. Ensures proper cleanup of all resources
|
|
319
|
-
|
|
364
|
+
|
|
320
365
|
Returns the agent's result or raises any exceptions that occurred.
|
|
321
366
|
"""
|
|
322
367
|
# Token tracking task reference for cleanup
|
|
323
368
|
token_tracking_task = None
|
|
324
|
-
|
|
369
|
+
|
|
325
370
|
try:
|
|
326
371
|
# Initialize the agent
|
|
327
372
|
agent = get_code_generation_agent()
|
|
328
|
-
|
|
373
|
+
|
|
329
374
|
# Start status display
|
|
330
375
|
status_display.start()
|
|
331
|
-
|
|
376
|
+
|
|
332
377
|
# Start token tracking
|
|
333
|
-
token_tracking_task = asyncio.create_task(
|
|
334
|
-
|
|
378
|
+
token_tracking_task = asyncio.create_task(
|
|
379
|
+
track_tokens_from_messages()
|
|
380
|
+
)
|
|
381
|
+
|
|
335
382
|
# Create a wrapper for the agent's run method
|
|
336
383
|
original_run = agent.run
|
|
337
|
-
|
|
384
|
+
|
|
338
385
|
async def wrapped_run(*args, **kwargs):
|
|
339
386
|
return await wrap_agent_run(original_run, *args, **kwargs)
|
|
340
|
-
|
|
387
|
+
|
|
341
388
|
agent.run = wrapped_run
|
|
342
|
-
|
|
389
|
+
|
|
343
390
|
# Run the agent with MCP servers
|
|
344
391
|
async with agent.run_mcp_servers():
|
|
345
392
|
result = await agent.run(
|
|
346
|
-
task,
|
|
347
|
-
message_history=get_message_history()
|
|
393
|
+
task, message_history=get_message_history()
|
|
348
394
|
)
|
|
349
395
|
return result
|
|
350
396
|
except Exception as e:
|
|
@@ -358,10 +404,9 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
358
404
|
token_tracking_task.cancel()
|
|
359
405
|
if not agent_task.done():
|
|
360
406
|
set_message_history(
|
|
361
|
-
message_history_processor(
|
|
362
|
-
get_message_history()
|
|
363
|
-
)
|
|
407
|
+
message_history_processor(get_message_history())
|
|
364
408
|
)
|
|
409
|
+
|
|
365
410
|
agent_task = asyncio.create_task(run_agent_task())
|
|
366
411
|
|
|
367
412
|
import signal
|
|
@@ -371,6 +416,7 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
371
416
|
|
|
372
417
|
# Ensure the interrupt handler only acts once per task
|
|
373
418
|
handled = False
|
|
419
|
+
|
|
374
420
|
def keyboard_interrupt_handler(sig, frame):
|
|
375
421
|
nonlocal local_cancelled
|
|
376
422
|
nonlocal handled
|
|
@@ -381,7 +427,9 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
381
427
|
try:
|
|
382
428
|
killed = kill_all_running_shell_processes()
|
|
383
429
|
if killed:
|
|
384
|
-
console.print(
|
|
430
|
+
console.print(
|
|
431
|
+
f"[yellow]Cancelled {killed} running shell process(es).[/yellow]"
|
|
432
|
+
)
|
|
385
433
|
else:
|
|
386
434
|
# Then cancel the agent task
|
|
387
435
|
if not agent_task.done():
|
|
@@ -392,6 +440,7 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
392
440
|
# On Windows, we need to reset the signal handler to avoid weird terminal behavior
|
|
393
441
|
if sys.platform.startswith("win"):
|
|
394
442
|
signal.signal(signal.SIGINT, original_handler or signal.SIG_DFL)
|
|
443
|
+
|
|
395
444
|
try:
|
|
396
445
|
original_handler = signal.getsignal(signal.SIGINT)
|
|
397
446
|
signal.signal(signal.SIGINT, keyboard_interrupt_handler)
|
|
@@ -412,13 +461,15 @@ async def interactive_mode(history_file_path: str) -> None:
|
|
|
412
461
|
if status_display.is_active:
|
|
413
462
|
status_display.stop()
|
|
414
463
|
else:
|
|
415
|
-
if result is not None and hasattr(result,
|
|
464
|
+
if result is not None and hasattr(result, "output"):
|
|
416
465
|
agent_response = result.output
|
|
417
466
|
console.print(agent_response)
|
|
418
467
|
filtered = message_history_processor(get_message_history())
|
|
419
468
|
set_message_history(filtered)
|
|
420
469
|
else:
|
|
421
|
-
console.print(
|
|
470
|
+
console.print(
|
|
471
|
+
"[yellow]No result received from the agent[/yellow]"
|
|
472
|
+
)
|
|
422
473
|
# Still process history if possible
|
|
423
474
|
filtered = message_history_processor(get_message_history())
|
|
424
475
|
set_message_history(filtered)
|
|
@@ -20,6 +20,7 @@ from code_puppy.token_utils import estimate_tokens
|
|
|
20
20
|
# Import the status display to get token rate info
|
|
21
21
|
try:
|
|
22
22
|
from code_puppy.status_display import StatusDisplay
|
|
23
|
+
|
|
23
24
|
STATUS_DISPLAY_AVAILABLE = True
|
|
24
25
|
except ImportError:
|
|
25
26
|
STATUS_DISPLAY_AVAILABLE = False
|
|
@@ -160,9 +161,8 @@ def summarize_message(message: ModelMessage) -> ModelMessage:
|
|
|
160
161
|
content_bits.append(s)
|
|
161
162
|
if not content_bits:
|
|
162
163
|
return message
|
|
163
|
-
prompt = (
|
|
164
|
-
|
|
165
|
-
+ "\n".join(content_bits)
|
|
164
|
+
prompt = "Please summarize the following user message:\n" + "\n".join(
|
|
165
|
+
content_bits
|
|
166
166
|
)
|
|
167
167
|
agent = get_summarization_agent()
|
|
168
168
|
result = agent.run_sync(prompt)
|
|
@@ -194,6 +194,7 @@ def get_model_context_length() -> int:
|
|
|
194
194
|
# Reserve 10% of context for response
|
|
195
195
|
return int(context_length)
|
|
196
196
|
|
|
197
|
+
|
|
197
198
|
def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
198
199
|
"""
|
|
199
200
|
Remove any messages that participate in mismatched tool call sequences.
|
|
@@ -240,7 +241,9 @@ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMess
|
|
|
240
241
|
pruned.append(msg)
|
|
241
242
|
|
|
242
243
|
if dropped_count:
|
|
243
|
-
console.print(
|
|
244
|
+
console.print(
|
|
245
|
+
f"[yellow]Pruned {dropped_count} message(s) with mismatched tool_call_id pairs[/yellow]"
|
|
246
|
+
)
|
|
244
247
|
return pruned
|
|
245
248
|
|
|
246
249
|
|
|
@@ -251,7 +254,7 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
|
|
|
251
254
|
model_max = get_model_context_length()
|
|
252
255
|
|
|
253
256
|
proportion_used = total_current_tokens / model_max
|
|
254
|
-
|
|
257
|
+
|
|
255
258
|
# Include token per second rate if available
|
|
256
259
|
token_rate_info = ""
|
|
257
260
|
if STATUS_DISPLAY_AVAILABLE:
|
|
@@ -262,12 +265,12 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
|
|
|
262
265
|
token_rate_info = f", {current_rate:.0f} t/s"
|
|
263
266
|
else:
|
|
264
267
|
token_rate_info = f", {current_rate:.1f} t/s"
|
|
265
|
-
|
|
268
|
+
|
|
266
269
|
# Print blue status bar - ALWAYS at top
|
|
267
270
|
console.print(f"""
|
|
268
271
|
[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}{token_rate_info}
|
|
269
272
|
""")
|
|
270
|
-
|
|
273
|
+
|
|
271
274
|
# Print extra line to ensure separation
|
|
272
275
|
console.print("\n")
|
|
273
276
|
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import random
|
|
3
2
|
import time
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from typing import List, Optional
|
|
6
3
|
|
|
7
|
-
from rich.console import Console
|
|
4
|
+
from rich.console import Console
|
|
8
5
|
from rich.live import Live
|
|
9
6
|
from rich.panel import Panel
|
|
10
7
|
from rich.spinner import Spinner
|
|
@@ -45,7 +42,7 @@ class StatusDisplay:
|
|
|
45
42
|
"Howling at the code...",
|
|
46
43
|
"Snuggling up to the task...",
|
|
47
44
|
"Bounding through data...",
|
|
48
|
-
"Puppy pondering..."
|
|
45
|
+
"Puppy pondering...",
|
|
49
46
|
]
|
|
50
47
|
self.current_message_index = 0
|
|
51
48
|
self.spinner = Spinner("dots", text="")
|
|
@@ -63,21 +60,23 @@ class StatusDisplay:
|
|
|
63
60
|
self.current_rate = (self.current_rate * 0.7) + (rate * 0.3)
|
|
64
61
|
else:
|
|
65
62
|
self.current_rate = rate
|
|
66
|
-
|
|
63
|
+
|
|
67
64
|
# Only ensure rate is not negative
|
|
68
65
|
self.current_rate = max(0, self.current_rate)
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
# Update the global rate for other components to access
|
|
71
68
|
global CURRENT_TOKEN_RATE
|
|
72
69
|
CURRENT_TOKEN_RATE = self.current_rate
|
|
73
|
-
|
|
70
|
+
|
|
74
71
|
self.last_update_time = current_time
|
|
75
72
|
self.last_token_count = self.token_count
|
|
76
73
|
return self.current_rate
|
|
77
|
-
|
|
78
|
-
def update_rate_from_sse(
|
|
74
|
+
|
|
75
|
+
def update_rate_from_sse(
|
|
76
|
+
self, completion_tokens: int, completion_time: float
|
|
77
|
+
) -> None:
|
|
79
78
|
"""Update the token rate directly using SSE time_info data
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
Args:
|
|
82
81
|
completion_tokens: Number of tokens in the completion (from SSE stream)
|
|
83
82
|
completion_time: Time taken for completion in seconds (from SSE stream)
|
|
@@ -85,17 +84,19 @@ class StatusDisplay:
|
|
|
85
84
|
if completion_time > 0:
|
|
86
85
|
# Using the direct t/s formula: tokens / time
|
|
87
86
|
rate = completion_tokens / completion_time
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Use a lighter smoothing for this more accurate data
|
|
90
89
|
if self.current_rate > 0:
|
|
91
|
-
self.current_rate = (self.current_rate * 0.3) + (
|
|
90
|
+
self.current_rate = (self.current_rate * 0.3) + (
|
|
91
|
+
rate * 0.7
|
|
92
|
+
) # Weight SSE data more heavily
|
|
92
93
|
else:
|
|
93
94
|
self.current_rate = rate
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
# Update the global rate
|
|
96
97
|
global CURRENT_TOKEN_RATE
|
|
97
98
|
CURRENT_TOKEN_RATE = self.current_rate
|
|
98
|
-
|
|
99
|
+
|
|
99
100
|
@staticmethod
|
|
100
101
|
def get_current_rate() -> float:
|
|
101
102
|
"""Get the current token rate for use in other components"""
|
|
@@ -111,7 +112,7 @@ class StatusDisplay:
|
|
|
111
112
|
# Reset token counters for new task
|
|
112
113
|
self.last_token_count = 0
|
|
113
114
|
self.current_rate = 0.0
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
# Allow for incremental updates (common for streaming) or absolute updates
|
|
116
117
|
if tokens > self.token_count or tokens < 0:
|
|
117
118
|
# Incremental update or reset
|
|
@@ -120,64 +121,75 @@ class StatusDisplay:
|
|
|
120
121
|
# If tokens <= current count but > 0, treat as incremental
|
|
121
122
|
# This handles simulated token streaming
|
|
122
123
|
self.token_count += tokens
|
|
123
|
-
|
|
124
|
+
|
|
124
125
|
self._calculate_rate()
|
|
125
126
|
|
|
126
127
|
def _get_status_panel(self) -> Panel:
|
|
127
128
|
"""Generate a status panel with current rate and animated message"""
|
|
128
|
-
rate_text =
|
|
129
|
-
|
|
129
|
+
rate_text = (
|
|
130
|
+
f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
|
|
131
|
+
)
|
|
132
|
+
|
|
130
133
|
# Update spinner
|
|
131
134
|
self.spinner.update()
|
|
132
|
-
|
|
135
|
+
|
|
133
136
|
# Rotate through loading messages every few updates
|
|
134
137
|
if int(time.time() * 2) % 4 == 0:
|
|
135
|
-
self.current_message_index = (self.current_message_index + 1) % len(
|
|
136
|
-
|
|
138
|
+
self.current_message_index = (self.current_message_index + 1) % len(
|
|
139
|
+
self.loading_messages
|
|
140
|
+
)
|
|
141
|
+
|
|
137
142
|
# Create a highly visible status message
|
|
138
143
|
status_text = Text.assemble(
|
|
139
144
|
Text(f"⏳ {rate_text} ", style="bold cyan"),
|
|
140
145
|
self.spinner,
|
|
141
|
-
Text(
|
|
146
|
+
Text(
|
|
147
|
+
f" {self.loading_messages[self.current_message_index]} ⏳",
|
|
148
|
+
style="bold yellow",
|
|
149
|
+
),
|
|
142
150
|
)
|
|
143
|
-
|
|
151
|
+
|
|
144
152
|
# Use expanded panel with more visible formatting
|
|
145
153
|
return Panel(
|
|
146
|
-
status_text,
|
|
147
|
-
title="[bold blue]Code Puppy Status[/bold blue]",
|
|
154
|
+
status_text,
|
|
155
|
+
title="[bold blue]Code Puppy Status[/bold blue]",
|
|
148
156
|
border_style="bright_blue",
|
|
149
157
|
expand=False,
|
|
150
|
-
padding=(1, 2)
|
|
158
|
+
padding=(1, 2),
|
|
151
159
|
)
|
|
152
160
|
|
|
153
161
|
def _get_status_text(self) -> Text:
|
|
154
162
|
"""Generate a status text with current rate and animated message"""
|
|
155
|
-
rate_text =
|
|
156
|
-
|
|
163
|
+
rate_text = (
|
|
164
|
+
f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
|
|
165
|
+
)
|
|
166
|
+
|
|
157
167
|
# Update spinner
|
|
158
168
|
self.spinner.update()
|
|
159
|
-
|
|
169
|
+
|
|
160
170
|
# Rotate through loading messages
|
|
161
|
-
self.current_message_index = (self.current_message_index + 1) % len(
|
|
171
|
+
self.current_message_index = (self.current_message_index + 1) % len(
|
|
172
|
+
self.loading_messages
|
|
173
|
+
)
|
|
162
174
|
message = self.loading_messages[self.current_message_index]
|
|
163
|
-
|
|
175
|
+
|
|
164
176
|
# Create a highly visible status text
|
|
165
177
|
return Text.assemble(
|
|
166
178
|
Text(f"⏳ {rate_text} 🐾", style="bold cyan"),
|
|
167
|
-
Text(f" {message}", style="yellow")
|
|
179
|
+
Text(f" {message}", style="yellow"),
|
|
168
180
|
)
|
|
169
|
-
|
|
181
|
+
|
|
170
182
|
async def _update_display(self) -> None:
|
|
171
183
|
"""Update the display continuously while active using Rich Live display"""
|
|
172
184
|
# Add a newline to ensure we're below the blue bar
|
|
173
185
|
self.console.print("\n")
|
|
174
|
-
|
|
186
|
+
|
|
175
187
|
# Create a Live display that will update in-place
|
|
176
188
|
with Live(
|
|
177
|
-
self._get_status_text(),
|
|
189
|
+
self._get_status_text(),
|
|
178
190
|
console=self.console,
|
|
179
191
|
refresh_per_second=2, # Update twice per second
|
|
180
|
-
transient=False
|
|
192
|
+
transient=False, # Keep the final state visible
|
|
181
193
|
) as live:
|
|
182
194
|
# Keep updating the live display while active
|
|
183
195
|
while self.is_active:
|
|
@@ -202,19 +214,21 @@ class StatusDisplay:
|
|
|
202
214
|
if self.task:
|
|
203
215
|
self.task.cancel()
|
|
204
216
|
self.task = None
|
|
205
|
-
|
|
217
|
+
|
|
206
218
|
# Print final stats
|
|
207
219
|
elapsed = time.time() - self.start_time if self.start_time else 0
|
|
208
220
|
avg_rate = self.token_count / elapsed if elapsed > 0 else 0
|
|
209
|
-
self.console.print(
|
|
210
|
-
|
|
221
|
+
self.console.print(
|
|
222
|
+
f"[dim]Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)[/dim]"
|
|
223
|
+
)
|
|
224
|
+
|
|
211
225
|
# Reset state
|
|
212
226
|
self.start_time = None
|
|
213
227
|
self.token_count = 0
|
|
214
228
|
self.last_update_time = None
|
|
215
229
|
self.last_token_count = 0
|
|
216
230
|
self.current_rate = 0
|
|
217
|
-
|
|
231
|
+
|
|
218
232
|
# Reset global rate to 0 to avoid affecting subsequent tasks
|
|
219
233
|
global CURRENT_TOKEN_RATE
|
|
220
234
|
CURRENT_TOKEN_RATE = 0.0
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from code_puppy.tools.command_runner import (
|
|
2
2
|
register_command_runner_tools,
|
|
3
|
-
kill_all_running_shell_processes,
|
|
4
3
|
)
|
|
5
4
|
from code_puppy.tools.file_modifications import register_file_modifications_tools
|
|
6
5
|
from code_puppy.tools.file_operations import register_file_operations_tools
|
|
@@ -23,6 +23,7 @@ _RUNNING_PROCESSES: Set[subprocess.Popen] = set()
|
|
|
23
23
|
_RUNNING_PROCESSES_LOCK = threading.Lock()
|
|
24
24
|
_USER_KILLED_PROCESSES = set()
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
def _register_process(proc: subprocess.Popen) -> None:
|
|
27
28
|
with _RUNNING_PROCESSES_LOCK:
|
|
28
29
|
_RUNNING_PROCESSES.add(proc)
|
|
@@ -279,7 +280,7 @@ def run_shell_command_streaming(
|
|
|
279
280
|
exit_code=exit_code,
|
|
280
281
|
execution_time=execution_time,
|
|
281
282
|
timeout=False,
|
|
282
|
-
user_interrupted=process.pid in _USER_KILLED_PROCESSES
|
|
283
|
+
user_interrupted=process.pid in _USER_KILLED_PROCESSES,
|
|
283
284
|
)
|
|
284
285
|
return ShellCommandOutput(
|
|
285
286
|
success=exit_code == 0,
|
|
@@ -380,7 +381,9 @@ def run_shell_command(
|
|
|
380
381
|
)
|
|
381
382
|
_register_process(process)
|
|
382
383
|
try:
|
|
383
|
-
return run_shell_command_streaming(
|
|
384
|
+
return run_shell_command_streaming(
|
|
385
|
+
process, timeout=timeout, command=command
|
|
386
|
+
)
|
|
384
387
|
finally:
|
|
385
388
|
# Ensure unregistration in case streaming returned early or raised
|
|
386
389
|
_unregister_process(process)
|
|
@@ -77,6 +77,7 @@ IGNORE_PATTERNS = [
|
|
|
77
77
|
"**/.parcel-cache/**",
|
|
78
78
|
"**/.vite/**",
|
|
79
79
|
"**/storybook-static/**",
|
|
80
|
+
"**/*.tsbuildinfo/*",
|
|
80
81
|
# Python
|
|
81
82
|
"**/__pycache__/**",
|
|
82
83
|
"**/__pycache__",
|
|
@@ -317,7 +318,7 @@ IGNORE_PATTERNS = [
|
|
|
317
318
|
"**/*.old",
|
|
318
319
|
"**/*.save",
|
|
319
320
|
# Hidden files (but be careful with this one)
|
|
320
|
-
|
|
321
|
+
"**/.*", # Commented out as it might be too aggressive
|
|
321
322
|
]
|
|
322
323
|
|
|
323
324
|
|
|
@@ -9,6 +9,7 @@ from pydantic_ai import RunContext
|
|
|
9
9
|
from code_puppy.tools.common import console
|
|
10
10
|
from code_puppy.token_utils import estimate_tokens
|
|
11
11
|
from code_puppy.tools.token_check import token_guard
|
|
12
|
+
|
|
12
13
|
# ---------------------------------------------------------------------------
|
|
13
14
|
# Module-level helper functions (exposed for unit tests _and_ used as tools)
|
|
14
15
|
# ---------------------------------------------------------------------------
|
|
@@ -186,15 +187,20 @@ class ReadFileOutput(BaseModel):
|
|
|
186
187
|
error: str | None = None
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
def _read_file(
|
|
190
|
+
def _read_file(
|
|
191
|
+
context: RunContext,
|
|
192
|
+
file_path: str,
|
|
193
|
+
start_line: int | None = None,
|
|
194
|
+
num_lines: int | None = None,
|
|
195
|
+
) -> ReadFileOutput:
|
|
190
196
|
file_path = os.path.abspath(file_path)
|
|
191
|
-
|
|
197
|
+
|
|
192
198
|
# Build console message with optional parameters
|
|
193
199
|
console_msg = f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
|
|
194
200
|
if start_line is not None and num_lines is not None:
|
|
195
201
|
console_msg += f" [dim](lines {start_line}-{start_line + num_lines - 1})[/dim]"
|
|
196
202
|
console.print(console_msg)
|
|
197
|
-
|
|
203
|
+
|
|
198
204
|
console.print("[dim]" + "-" * 60 + "[/dim]")
|
|
199
205
|
if not os.path.exists(file_path):
|
|
200
206
|
error_msg = f"File {file_path} does not exist"
|
|
@@ -213,14 +219,16 @@ def _read_file(context: RunContext, file_path: str, start_line: int | None = Non
|
|
|
213
219
|
# Ensure indices are within bounds
|
|
214
220
|
start_idx = max(0, start_idx)
|
|
215
221
|
end_idx = min(len(lines), end_idx)
|
|
216
|
-
content =
|
|
222
|
+
content = "".join(lines[start_idx:end_idx])
|
|
217
223
|
else:
|
|
218
224
|
# Read the entire file
|
|
219
225
|
content = f.read()
|
|
220
226
|
|
|
221
227
|
num_tokens = estimate_tokens(content)
|
|
222
228
|
if num_tokens > 10000:
|
|
223
|
-
raise ValueError(
|
|
229
|
+
raise ValueError(
|
|
230
|
+
"The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks."
|
|
231
|
+
)
|
|
224
232
|
token_guard(num_tokens)
|
|
225
233
|
return ReadFileOutput(content=content, num_tokens=num_tokens)
|
|
226
234
|
except (FileNotFoundError, PermissionError):
|
|
@@ -270,7 +278,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
270
278
|
**{
|
|
271
279
|
"file_path": file_path,
|
|
272
280
|
"line_number": line_number,
|
|
273
|
-
"line_content": line_content.rstrip("\n\r")[
|
|
281
|
+
"line_content": line_content.rstrip("\n\r")[512:],
|
|
274
282
|
}
|
|
275
283
|
)
|
|
276
284
|
matches.append(match_info)
|
|
@@ -316,12 +324,17 @@ def list_files(
|
|
|
316
324
|
if num_tokens > 10000:
|
|
317
325
|
return ListFileOutput(
|
|
318
326
|
files=[],
|
|
319
|
-
error="Too many files - tokens exceeded. Try listing non-recursively"
|
|
327
|
+
error="Too many files - tokens exceeded. Try listing non-recursively",
|
|
320
328
|
)
|
|
321
329
|
return list_files_output
|
|
322
330
|
|
|
323
331
|
|
|
324
|
-
def read_file(
|
|
332
|
+
def read_file(
|
|
333
|
+
context: RunContext,
|
|
334
|
+
file_path: str = "",
|
|
335
|
+
start_line: int | None = None,
|
|
336
|
+
num_lines: int | None = None,
|
|
337
|
+
) -> ReadFileOutput:
|
|
325
338
|
return _read_file(context, file_path, start_line, num_lines)
|
|
326
339
|
|
|
327
340
|
|
|
@@ -4,8 +4,13 @@ from code_puppy.token_utils import estimate_tokens_for_message
|
|
|
4
4
|
|
|
5
5
|
def token_guard(num_tokens: int):
|
|
6
6
|
from code_puppy import state_management
|
|
7
|
+
|
|
7
8
|
current_history = state_management.get_message_history()
|
|
8
|
-
message_hist_tokens = sum(
|
|
9
|
+
message_hist_tokens = sum(
|
|
10
|
+
estimate_tokens_for_message(msg) for msg in current_history
|
|
11
|
+
)
|
|
9
12
|
|
|
10
13
|
if message_hist_tokens + num_tokens > (get_model_context_length() * 0.9):
|
|
11
|
-
raise ValueError(
|
|
14
|
+
raise ValueError(
|
|
15
|
+
"Tokens produced by this tool call would exceed model capacity"
|
|
16
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|