code-puppy 0.0.88__tar.gz → 0.0.90__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.
Files changed (31) hide show
  1. {code_puppy-0.0.88 → code_puppy-0.0.90}/PKG-INFO +1 -1
  2. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/main.py +169 -8
  3. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/message_history_processor.py +24 -1
  4. code_puppy-0.0.90/code_puppy/status_display.py +209 -0
  5. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/file_operations.py +11 -2
  6. {code_puppy-0.0.88 → code_puppy-0.0.90}/pyproject.toml +1 -1
  7. {code_puppy-0.0.88 → code_puppy-0.0.90}/.gitignore +0 -0
  8. {code_puppy-0.0.88 → code_puppy-0.0.90}/LICENSE +0 -0
  9. {code_puppy-0.0.88 → code_puppy-0.0.90}/README.md +0 -0
  10. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/agent.py +0 -0
  12. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/agent_prompts.py +0 -0
  13. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/__init__.py +0 -0
  14. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/file_path_completion.py +0 -0
  15. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/meta_command_handler.py +0 -0
  16. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/model_picker_completion.py +0 -0
  17. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/motd.py +0 -0
  18. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  19. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/command_line/utils.py +0 -0
  20. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/config.py +0 -0
  21. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/model_factory.py +0 -0
  22. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/models.json +0 -0
  23. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/state_management.py +0 -0
  24. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/summarization_agent.py +0 -0
  25. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/token_utils.py +0 -0
  26. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/__init__.py +0 -0
  27. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/command_runner.py +0 -0
  28. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/common.py +0 -0
  29. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/file_modifications.py +0 -0
  30. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/tools/token_check.py +0 -0
  31. {code_puppy-0.0.88 → code_puppy-0.0.90}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.88
3
+ Version: 0.0.90
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -1,6 +1,7 @@
1
1
  import argparse
2
2
  import asyncio
3
3
  import os
4
+ import random
4
5
  import sys
5
6
 
6
7
  from dotenv import load_dotenv
@@ -17,6 +18,7 @@ from code_puppy.command_line.prompt_toolkit_completion import (
17
18
  )
18
19
  from code_puppy.config import ensure_config_exists
19
20
  from code_puppy.state_management import get_message_history, set_message_history
21
+ from code_puppy.status_display import StatusDisplay
20
22
 
21
23
  # Initialize rich console for pretty output
22
24
  from code_puppy.tools.common import console
@@ -194,17 +196,167 @@ async def interactive_mode(history_file_path: str) -> None:
194
196
  try:
195
197
  prettier_code_blocks()
196
198
  local_cancelled = False
197
-
199
+
200
+ # Initialize status display for tokens per second and loading messages
201
+ status_display = StatusDisplay(console)
202
+
203
+ # Print a message indicating we're about to start processing
204
+ console.print("\nStarting task processing...")
205
+
206
+ async def track_tokens_from_messages():
207
+ """
208
+ Track real token counts from message history.
209
+
210
+ This async function runs in the background and periodically checks
211
+ the message history for new tokens. When new tokens are detected,
212
+ it updates the StatusDisplay with the incremental count to calculate
213
+ an accurate tokens-per-second rate.
214
+
215
+ It also looks for SSE stream time_info data to get precise token rate
216
+ calculations using the formula: completion_tokens * 1 / completion_time
217
+
218
+ The function continues running until status_display.is_active becomes False.
219
+ """
220
+ from code_puppy.message_history_processor import estimate_tokens_for_message
221
+ import json
222
+ import re
223
+
224
+ last_token_total = 0
225
+ last_sse_data = None
226
+
227
+ while status_display.is_active:
228
+ # Get real token count from message history
229
+ messages = get_message_history()
230
+ if messages:
231
+ # Calculate total tokens across all messages
232
+ current_token_total = sum(estimate_tokens_for_message(msg) for msg in messages)
233
+
234
+ # If tokens increased, update the display with the incremental count
235
+ if current_token_total > last_token_total:
236
+ status_display.update_token_count(current_token_total - last_token_total)
237
+ last_token_total = current_token_total
238
+
239
+ # Try to find SSE stream data in assistant messages
240
+ for msg in messages:
241
+ # Handle different message types (dict or ModelMessage objects)
242
+ if hasattr(msg, 'role') and msg.role == 'assistant':
243
+ # ModelMessage object with role attribute
244
+ content = msg.content if hasattr(msg, 'content') else ''
245
+ elif isinstance(msg, dict) and msg.get('role') == 'assistant':
246
+ # Dictionary with 'role' key
247
+ content = msg.get('content', '')
248
+ # Support for ModelRequest/ModelResponse objects
249
+ elif hasattr(msg, 'message') and hasattr(msg.message, 'role') and msg.message.role == 'assistant':
250
+ # Access content through the message attribute
251
+ content = msg.message.content if hasattr(msg.message, 'content') else ''
252
+ else:
253
+ # Skip if not an assistant message or unrecognized format
254
+ continue
255
+
256
+ # Convert content to string if it's not already
257
+ if not isinstance(content, str):
258
+ try:
259
+ content = str(content)
260
+ except:
261
+ continue
262
+
263
+ # Look for SSE usage data pattern in the message content
264
+ sse_matches = re.findall(r'\{\s*"usage".*?"time_info".*?\}', content, re.DOTALL)
265
+ for match in sse_matches:
266
+ try:
267
+ # Parse the JSON data
268
+ sse_data = json.loads(match)
269
+ if sse_data != last_sse_data: # Only process new data
270
+ # Check if we have time_info and completion_tokens
271
+ if 'time_info' in sse_data and 'completion_time' in sse_data['time_info'] and \
272
+ 'usage' in sse_data and 'completion_tokens' in sse_data['usage']:
273
+ completion_time = float(sse_data['time_info']['completion_time'])
274
+ completion_tokens = int(sse_data['usage']['completion_tokens'])
275
+
276
+ # Update rate using the accurate SSE data
277
+ if completion_time > 0 and completion_tokens > 0:
278
+ status_display.update_rate_from_sse(completion_tokens, completion_time)
279
+ last_sse_data = sse_data
280
+ except (json.JSONDecodeError, KeyError, ValueError):
281
+ # Ignore parsing errors and continue
282
+ pass
283
+
284
+ # Small sleep interval for responsive updates without excessive CPU usage
285
+ await asyncio.sleep(0.1)
286
+
287
+ async def wrap_agent_run(original_run, *args, **kwargs):
288
+ """
289
+ Wraps the agent's run method to enable token tracking.
290
+
291
+ This wrapper preserves the original functionality while allowing
292
+ us to track tokens as they are generated by the model. No additional
293
+ logic is needed here since the token tracking happens in a separate task.
294
+
295
+ Args:
296
+ original_run: The original agent.run method
297
+ *args, **kwargs: Arguments to pass to the original run method
298
+
299
+ Returns:
300
+ The result from the original run method
301
+ """
302
+ result = await original_run(*args, **kwargs)
303
+ return result
304
+
198
305
  async def run_agent_task():
306
+ """
307
+ Main task runner for the agent with token tracking.
308
+
309
+ This function:
310
+ 1. Sets up the agent with token tracking
311
+ 2. Starts the status display showing token rate
312
+ 3. Runs the agent with the user's task
313
+ 4. Ensures proper cleanup of all resources
314
+
315
+ Returns the agent's result or raises any exceptions that occurred.
316
+ """
317
+ # Token tracking task reference for cleanup
318
+ token_tracking_task = None
319
+
199
320
  try:
321
+ # Initialize the agent
200
322
  agent = get_code_generation_agent()
323
+
324
+ # Start status display
325
+ status_display.start()
326
+
327
+ # Start token tracking
328
+ token_tracking_task = asyncio.create_task(track_tokens_from_messages())
329
+
330
+ # Create a wrapper for the agent's run method
331
+ original_run = agent.run
332
+
333
+ async def wrapped_run(*args, **kwargs):
334
+ return await wrap_agent_run(original_run, *args, **kwargs)
335
+
336
+ agent.run = wrapped_run
337
+
338
+ # Run the agent with MCP servers
201
339
  async with agent.run_mcp_servers():
202
- return await agent.run(
203
- task, message_history=get_message_history()
340
+ result = await agent.run(
341
+ task,
342
+ message_history=get_message_history()
204
343
  )
344
+ return result
205
345
  except Exception as e:
206
346
  console.log("Task failed", e)
207
-
347
+ raise
348
+ finally:
349
+ # Clean up resources
350
+ if status_display.is_active:
351
+ status_display.stop()
352
+ if token_tracking_task and not token_tracking_task.done():
353
+ token_tracking_task.cancel()
354
+ if not agent_task.done():
355
+ set_message_history(
356
+ message_history_processor(
357
+ get_message_history()
358
+ )
359
+ )
208
360
  agent_task = asyncio.create_task(run_agent_task())
209
361
 
210
362
  import signal
@@ -251,11 +403,20 @@ async def interactive_mode(history_file_path: str) -> None:
251
403
 
252
404
  if local_cancelled:
253
405
  console.print("Task canceled by user")
406
+ # Ensure status display is stopped if canceled
407
+ if status_display.is_active:
408
+ status_display.stop()
254
409
  else:
255
- agent_response = result.output
256
- console.print(agent_response)
257
- filtered = message_history_processor(get_message_history())
258
- set_message_history(filtered)
410
+ if result is not None and hasattr(result, 'output'):
411
+ agent_response = result.output
412
+ console.print(agent_response)
413
+ filtered = message_history_processor(get_message_history())
414
+ set_message_history(filtered)
415
+ else:
416
+ console.print("[yellow]No result received from the agent[/yellow]")
417
+ # Still process history if possible
418
+ filtered = message_history_processor(get_message_history())
419
+ set_message_history(filtered)
259
420
 
260
421
  # Show context status
261
422
  console.print(
@@ -17,6 +17,13 @@ from code_puppy.tools.common import console
17
17
  from code_puppy.model_factory import ModelFactory
18
18
  from code_puppy.config import get_model_name
19
19
 
20
+ # Import the status display to get token rate info
21
+ try:
22
+ from code_puppy.status_display import StatusDisplay
23
+ STATUS_DISPLAY_AVAILABLE = True
24
+ except ImportError:
25
+ STATUS_DISPLAY_AVAILABLE = False
26
+
20
27
  # Import summarization agent
21
28
  try:
22
29
  from code_puppy.summarization_agent import (
@@ -246,9 +253,25 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
246
253
  model_max = get_model_context_length()
247
254
 
248
255
  proportion_used = total_current_tokens / model_max
256
+
257
+ # Include token per second rate if available
258
+ token_rate_info = ""
259
+ if STATUS_DISPLAY_AVAILABLE:
260
+ current_rate = StatusDisplay.get_current_rate()
261
+ if current_rate > 0:
262
+ # Format with improved precision when using SSE data
263
+ if current_rate > 1000:
264
+ token_rate_info = f", {current_rate:.0f} t/s"
265
+ else:
266
+ token_rate_info = f", {current_rate:.1f} t/s"
267
+
268
+ # Print blue status bar - ALWAYS at top
249
269
  console.print(f"""
250
- [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}
270
+ [bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f}{token_rate_info}
251
271
  """)
272
+
273
+ # Print extra line to ensure separation
274
+ console.print("\n")
252
275
 
253
276
  if proportion_used > 0.85:
254
277
  summary = summarize_messages(messages)
@@ -0,0 +1,209 @@
1
+ import asyncio
2
+ import random
3
+ import time
4
+ from datetime import datetime
5
+ from typing import List, Optional
6
+
7
+ from rich.console import Console, RenderableType
8
+ from rich.live import Live
9
+ from rich.panel import Panel
10
+ from rich.spinner import Spinner
11
+ from rich.text import Text
12
+
13
+ # Global variable to track current token per second rate
14
+ CURRENT_TOKEN_RATE = 0.0
15
+
16
+
17
+ class StatusDisplay:
18
+ """
19
+ Displays real-time status information during model execution,
20
+ including token per second rate and rotating loading messages.
21
+ """
22
+
23
+ def __init__(self, console: Console):
24
+ self.console = console
25
+ self.token_count = 0
26
+ self.start_time = None
27
+ self.last_update_time = None
28
+ self.last_token_count = 0
29
+ self.current_rate = 0
30
+ self.is_active = False
31
+ self.task = None
32
+ self.live = None
33
+ self.loading_messages = [
34
+ "Fetching...",
35
+ "Sniffing around...",
36
+ "Wagging tail...",
37
+ "Pawsing for a moment...",
38
+ "Chasing tail...",
39
+ "Digging up results...",
40
+ "Barking at the data...",
41
+ "Rolling over...",
42
+ "Panting with excitement...",
43
+ "Chewing on it...",
44
+ "Prancing along...",
45
+ "Howling at the code...",
46
+ "Snuggling up to the task...",
47
+ "Bounding through data...",
48
+ "Puppy pondering..."
49
+ ]
50
+ self.current_message_index = 0
51
+ self.spinner = Spinner("dots", text="")
52
+
53
+ def _calculate_rate(self) -> float:
54
+ """Calculate the current token rate"""
55
+ current_time = time.time()
56
+ if self.last_update_time:
57
+ time_diff = current_time - self.last_update_time
58
+ token_diff = self.token_count - self.last_token_count
59
+ if time_diff > 0:
60
+ rate = token_diff / time_diff
61
+ # Smooth the rate calculation with the current rate
62
+ if self.current_rate > 0:
63
+ self.current_rate = (self.current_rate * 0.7) + (rate * 0.3)
64
+ else:
65
+ self.current_rate = rate
66
+
67
+ # Only ensure rate is not negative
68
+ self.current_rate = max(0, self.current_rate)
69
+
70
+ # Update the global rate for other components to access
71
+ global CURRENT_TOKEN_RATE
72
+ CURRENT_TOKEN_RATE = self.current_rate
73
+
74
+ self.last_update_time = current_time
75
+ self.last_token_count = self.token_count
76
+ return self.current_rate
77
+
78
+ def update_rate_from_sse(self, completion_tokens: int, completion_time: float) -> None:
79
+ """Update the token rate directly using SSE time_info data
80
+
81
+ Args:
82
+ completion_tokens: Number of tokens in the completion (from SSE stream)
83
+ completion_time: Time taken for completion in seconds (from SSE stream)
84
+ """
85
+ if completion_time > 0:
86
+ # Using the direct t/s formula: tokens / time
87
+ rate = completion_tokens / completion_time
88
+
89
+ # Use a lighter smoothing for this more accurate data
90
+ if self.current_rate > 0:
91
+ self.current_rate = (self.current_rate * 0.3) + (rate * 0.7) # Weight SSE data more heavily
92
+ else:
93
+ self.current_rate = rate
94
+
95
+ # Update the global rate
96
+ global CURRENT_TOKEN_RATE
97
+ CURRENT_TOKEN_RATE = self.current_rate
98
+
99
+ @staticmethod
100
+ def get_current_rate() -> float:
101
+ """Get the current token rate for use in other components"""
102
+ global CURRENT_TOKEN_RATE
103
+ return CURRENT_TOKEN_RATE
104
+
105
+ def update_token_count(self, tokens: int) -> None:
106
+ """Update the token count and recalculate the rate"""
107
+ if self.start_time is None:
108
+ self.start_time = time.time()
109
+ self.last_update_time = self.start_time
110
+
111
+ # Allow for incremental updates (common for streaming) or absolute updates
112
+ if tokens > self.token_count or tokens < 0:
113
+ # Incremental update or reset
114
+ self.token_count = tokens if tokens >= 0 else 0
115
+ else:
116
+ # If tokens <= current count but > 0, treat as incremental
117
+ # This handles simulated token streaming
118
+ self.token_count += tokens
119
+
120
+ self._calculate_rate()
121
+
122
+ def _get_status_panel(self) -> Panel:
123
+ """Generate a status panel with current rate and animated message"""
124
+ rate_text = f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
125
+
126
+ # Update spinner
127
+ self.spinner.update()
128
+
129
+ # Rotate through loading messages every few updates
130
+ if int(time.time() * 2) % 4 == 0:
131
+ self.current_message_index = (self.current_message_index + 1) % len(self.loading_messages)
132
+
133
+ # Create a highly visible status message
134
+ status_text = Text.assemble(
135
+ Text(f"⏳ {rate_text} ", style="bold cyan"),
136
+ self.spinner,
137
+ Text(f" {self.loading_messages[self.current_message_index]} ⏳", style="bold yellow")
138
+ )
139
+
140
+ # Use expanded panel with more visible formatting
141
+ return Panel(
142
+ status_text,
143
+ title="[bold blue]Code Puppy Status[/bold blue]",
144
+ border_style="bright_blue",
145
+ expand=False,
146
+ padding=(1, 2)
147
+ )
148
+
149
+ def _get_status_text(self) -> Text:
150
+ """Generate a status text with current rate and animated message"""
151
+ rate_text = f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
152
+
153
+ # Update spinner
154
+ self.spinner.update()
155
+
156
+ # Rotate through loading messages
157
+ self.current_message_index = (self.current_message_index + 1) % len(self.loading_messages)
158
+ message = self.loading_messages[self.current_message_index]
159
+
160
+ # Create a highly visible status text
161
+ return Text.assemble(
162
+ Text(f"⏳ {rate_text} 🐾", style="bold cyan"),
163
+ Text(f" {message}", style="yellow")
164
+ )
165
+
166
+ async def _update_display(self) -> None:
167
+ """Update the display continuously while active using Rich Live display"""
168
+ # Add a newline to ensure we're below the blue bar
169
+ self.console.print("\n")
170
+
171
+ # Create a Live display that will update in-place
172
+ with Live(
173
+ self._get_status_text(),
174
+ console=self.console,
175
+ refresh_per_second=2, # Update twice per second
176
+ transient=False # Keep the final state visible
177
+ ) as live:
178
+ # Keep updating the live display while active
179
+ while self.is_active:
180
+ live.update(self._get_status_text())
181
+ await asyncio.sleep(0.5)
182
+
183
+ def start(self) -> None:
184
+ """Start the status display"""
185
+ if not self.is_active:
186
+ self.is_active = True
187
+ self.start_time = time.time()
188
+ self.last_update_time = self.start_time
189
+ self.token_count = 0
190
+ self.last_token_count = 0
191
+ self.current_rate = 0
192
+ self.task = asyncio.create_task(self._update_display())
193
+
194
+ def stop(self) -> None:
195
+ """Stop the status display"""
196
+ if self.is_active:
197
+ self.is_active = False
198
+ if self.task:
199
+ self.task.cancel()
200
+ self.task = None
201
+
202
+ # Print final stats
203
+ elapsed = time.time() - self.start_time if self.start_time else 0
204
+ avg_rate = self.token_count / elapsed if elapsed > 0 else 0
205
+ self.console.print(f"[dim]Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)[/dim]")
206
+
207
+ # Reset
208
+ self.start_time = None
209
+ self.token_count = 0
@@ -25,6 +25,7 @@ class ListedFile(BaseModel):
25
25
 
26
26
  class ListFileOutput(BaseModel):
27
27
  files: List[ListedFile]
28
+ error: str | None = None
28
29
 
29
30
 
30
31
  def _list_files(
@@ -270,7 +271,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
270
271
  **{
271
272
  "file_path": file_path,
272
273
  "line_number": line_number,
273
- "line_content": line_content.rstrip("\n\r"),
274
+ "line_content": line_content.rstrip("\n\r")[2048:],
274
275
  }
275
276
  )
276
277
  matches.append(match_info)
@@ -311,7 +312,15 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
311
312
  def list_files(
312
313
  context: RunContext, directory: str = ".", recursive: bool = True
313
314
  ) -> ListFileOutput:
314
- return _list_files(context, directory, recursive)
315
+ list_files_output = _list_files(context, directory, recursive)
316
+ tokenizer = get_tokenizer()
317
+ num_tokens = len(tokenizer.encode(list_files_output.model_dump_json()))
318
+ if num_tokens > 10000:
319
+ return ListFileOutput(
320
+ files=[],
321
+ error="Too many files - tokens exceeded. Try listing non-recursively"
322
+ )
323
+ return list_files_output
315
324
 
316
325
 
317
326
  def read_file(context: RunContext, file_path: str = "", start_line: int | None = None, num_lines: int | None = None) -> ReadFileOutput:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.88"
7
+ version = "0.0.90"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes