code-puppy 0.0.171__py3-none-any.whl → 0.0.173__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 +8 -8
- code_puppy/agents/agent_creator_agent.py +0 -3
- code_puppy/agents/agent_qa_kitten.py +203 -0
- code_puppy/agents/base_agent.py +398 -2
- code_puppy/command_line/command_handler.py +68 -28
- code_puppy/command_line/mcp/add_command.py +2 -2
- code_puppy/command_line/mcp/base.py +1 -1
- code_puppy/command_line/mcp/install_command.py +2 -2
- code_puppy/command_line/mcp/list_command.py +1 -1
- code_puppy/command_line/mcp/search_command.py +1 -1
- code_puppy/command_line/mcp/start_all_command.py +1 -1
- code_puppy/command_line/mcp/status_command.py +2 -2
- code_puppy/command_line/mcp/stop_all_command.py +1 -1
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +2 -2
- code_puppy/config.py +141 -12
- code_puppy/http_utils.py +50 -24
- code_puppy/main.py +2 -1
- code_puppy/{mcp → mcp_}/config_wizard.py +1 -1
- code_puppy/{mcp → mcp_}/examples/retry_example.py +1 -1
- code_puppy/{mcp → mcp_}/managed_server.py +1 -1
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +1 -3
- code_puppy/message_history_processor.py +83 -221
- code_puppy/messaging/message_queue.py +4 -4
- code_puppy/state_management.py +1 -100
- code_puppy/tools/__init__.py +103 -6
- code_puppy/tools/browser/__init__.py +0 -0
- code_puppy/tools/browser/browser_control.py +293 -0
- code_puppy/tools/browser/browser_interactions.py +552 -0
- code_puppy/tools/browser/browser_locators.py +642 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +242 -0
- code_puppy/tools/browser/browser_scripts.py +478 -0
- code_puppy/tools/browser/browser_workflows.py +196 -0
- code_puppy/tools/browser/camoufox_manager.py +194 -0
- code_puppy/tools/browser/vqa_agent.py +66 -0
- code_puppy/tools/browser_control.py +293 -0
- code_puppy/tools/browser_interactions.py +552 -0
- code_puppy/tools/browser_locators.py +642 -0
- code_puppy/tools/browser_navigation.py +251 -0
- code_puppy/tools/browser_screenshot.py +278 -0
- code_puppy/tools/browser_scripts.py +478 -0
- code_puppy/tools/browser_workflows.py +215 -0
- code_puppy/tools/camoufox_manager.py +150 -0
- code_puppy/tools/command_runner.py +13 -8
- code_puppy/tools/file_operations.py +7 -7
- code_puppy/tui/app.py +1 -1
- code_puppy/tui/components/custom_widgets.py +1 -1
- code_puppy/tui/screens/mcp_install_wizard.py +8 -8
- code_puppy/tui_state.py +55 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/METADATA +3 -1
- code_puppy-0.0.173.dist-info/RECORD +132 -0
- code_puppy-0.0.171.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/__init__.py +0 -0
- /code_puppy/{mcp → mcp_}/async_lifecycle.py +0 -0
- /code_puppy/{mcp → mcp_}/blocking_startup.py +0 -0
- /code_puppy/{mcp → mcp_}/captured_stdio_server.py +0 -0
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/dashboard.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/manager.py +0 -0
- /code_puppy/{mcp → mcp_}/registry.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.171.data → code_puppy-0.0.173.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/licenses/LICENSE +0 -0
|
@@ -81,7 +81,9 @@ def get_commands_help():
|
|
|
81
81
|
)
|
|
82
82
|
help_lines.append(
|
|
83
83
|
Text("/truncate", style="cyan")
|
|
84
|
-
+ Text(
|
|
84
|
+
+ Text(
|
|
85
|
+
" <N> Truncate message history to N most recent messages (keeping system message)"
|
|
86
|
+
)
|
|
85
87
|
)
|
|
86
88
|
help_lines.append(
|
|
87
89
|
Text("/<unknown>", style="cyan")
|
|
@@ -409,23 +411,33 @@ def handle_command(command: str):
|
|
|
409
411
|
|
|
410
412
|
if command.startswith("/pin_model"):
|
|
411
413
|
# Handle agent model pinning
|
|
414
|
+
import json
|
|
415
|
+
|
|
412
416
|
from code_puppy.agents.json_agent import discover_json_agents
|
|
413
417
|
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
414
|
-
import json
|
|
415
418
|
|
|
416
419
|
tokens = command.split()
|
|
417
420
|
|
|
418
421
|
if len(tokens) != 3:
|
|
419
422
|
emit_warning("Usage: /pin_model <agent-name> <model-name>")
|
|
420
423
|
|
|
421
|
-
# Show available models and
|
|
424
|
+
# Show available models and agents
|
|
422
425
|
available_models = load_model_names()
|
|
423
426
|
json_agents = discover_json_agents()
|
|
424
427
|
|
|
428
|
+
# Get built-in agents
|
|
429
|
+
from code_puppy.agents.agent_manager import get_agent_descriptions
|
|
430
|
+
builtin_agents = get_agent_descriptions()
|
|
431
|
+
|
|
425
432
|
emit_info("Available models:")
|
|
426
433
|
for model in available_models:
|
|
427
434
|
emit_info(f" [cyan]{model}[/cyan]")
|
|
428
435
|
|
|
436
|
+
if builtin_agents:
|
|
437
|
+
emit_info("\nAvailable built-in agents:")
|
|
438
|
+
for agent_name, description in builtin_agents.items():
|
|
439
|
+
emit_info(f" [cyan]{agent_name}[/cyan] - {description}")
|
|
440
|
+
|
|
429
441
|
if json_agents:
|
|
430
442
|
emit_info("\nAvailable JSON agents:")
|
|
431
443
|
for agent_name, agent_path in json_agents.items():
|
|
@@ -442,31 +454,51 @@ def handle_command(command: str):
|
|
|
442
454
|
emit_warning(f"Available models: {', '.join(available_models)}")
|
|
443
455
|
return True
|
|
444
456
|
|
|
445
|
-
# Check
|
|
457
|
+
# Check if this is a JSON agent or a built-in Python agent
|
|
446
458
|
json_agents = discover_json_agents()
|
|
447
|
-
if agent_name not in json_agents:
|
|
448
|
-
emit_error(f"JSON agent '{agent_name}' not found")
|
|
449
459
|
|
|
450
|
-
|
|
460
|
+
# Get list of available built-in agents
|
|
461
|
+
from code_puppy.agents.agent_manager import get_agent_descriptions
|
|
462
|
+
builtin_agents = get_agent_descriptions()
|
|
463
|
+
|
|
464
|
+
is_json_agent = agent_name in json_agents
|
|
465
|
+
is_builtin_agent = agent_name in builtin_agents
|
|
466
|
+
|
|
467
|
+
if not is_json_agent and not is_builtin_agent:
|
|
468
|
+
emit_error(f"Agent '{agent_name}' not found")
|
|
469
|
+
|
|
470
|
+
# Show available agents
|
|
471
|
+
if builtin_agents:
|
|
472
|
+
emit_info("Available built-in agents:")
|
|
473
|
+
for name, desc in builtin_agents.items():
|
|
474
|
+
emit_info(f" [cyan]{name}[/cyan] - {desc}")
|
|
475
|
+
|
|
451
476
|
if json_agents:
|
|
452
|
-
emit_info("
|
|
477
|
+
emit_info("\nAvailable JSON agents:")
|
|
453
478
|
for name, path in json_agents.items():
|
|
454
479
|
emit_info(f" [cyan]{name}[/cyan] ({path})")
|
|
455
480
|
return True
|
|
456
481
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
# Load, modify, and save the agent configuration
|
|
482
|
+
# Handle different agent types
|
|
460
483
|
try:
|
|
461
|
-
|
|
462
|
-
|
|
484
|
+
if is_json_agent:
|
|
485
|
+
# Handle JSON agent - modify the JSON file
|
|
486
|
+
agent_file_path = json_agents[agent_name]
|
|
487
|
+
|
|
488
|
+
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
489
|
+
agent_config = json.load(f)
|
|
463
490
|
|
|
464
|
-
|
|
465
|
-
|
|
491
|
+
# Set the model
|
|
492
|
+
agent_config["model"] = model_name
|
|
466
493
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
494
|
+
# Save the updated configuration
|
|
495
|
+
with open(agent_file_path, "w", encoding="utf-8") as f:
|
|
496
|
+
json.dump(agent_config, f, indent=2, ensure_ascii=False)
|
|
497
|
+
|
|
498
|
+
else:
|
|
499
|
+
# Handle built-in Python agent - store in config
|
|
500
|
+
from code_puppy.config import set_agent_pinned_model
|
|
501
|
+
set_agent_pinned_model(agent_name, model_name)
|
|
470
502
|
|
|
471
503
|
emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
|
|
472
504
|
|
|
@@ -622,9 +654,11 @@ def handle_command(command: str):
|
|
|
622
654
|
if command.startswith("/truncate"):
|
|
623
655
|
tokens = command.split()
|
|
624
656
|
if len(tokens) != 2:
|
|
625
|
-
emit_error(
|
|
657
|
+
emit_error(
|
|
658
|
+
"Usage: /truncate <N> (where N is the number of messages to keep)"
|
|
659
|
+
)
|
|
626
660
|
return True
|
|
627
|
-
|
|
661
|
+
|
|
628
662
|
try:
|
|
629
663
|
n = int(tokens[1])
|
|
630
664
|
if n < 1:
|
|
@@ -633,23 +667,29 @@ def handle_command(command: str):
|
|
|
633
667
|
except ValueError:
|
|
634
668
|
emit_error("N must be a valid integer")
|
|
635
669
|
return True
|
|
636
|
-
|
|
670
|
+
|
|
637
671
|
from code_puppy.state_management import get_message_history, set_message_history
|
|
638
|
-
|
|
672
|
+
|
|
639
673
|
history = get_message_history()
|
|
640
674
|
if not history:
|
|
641
675
|
emit_warning("No history to truncate yet. Ask me something first!")
|
|
642
676
|
return True
|
|
643
|
-
|
|
677
|
+
|
|
644
678
|
if len(history) <= n:
|
|
645
|
-
emit_info(
|
|
679
|
+
emit_info(
|
|
680
|
+
f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate."
|
|
681
|
+
)
|
|
646
682
|
return True
|
|
647
|
-
|
|
683
|
+
|
|
648
684
|
# Always keep the first message (system message) and then keep the N-1 most recent messages
|
|
649
|
-
truncated_history =
|
|
650
|
-
|
|
685
|
+
truncated_history = (
|
|
686
|
+
[history[0]] + history[-(n - 1) :] if n > 1 else [history[0]]
|
|
687
|
+
)
|
|
688
|
+
|
|
651
689
|
set_message_history(truncated_history)
|
|
652
|
-
emit_success(
|
|
690
|
+
emit_success(
|
|
691
|
+
f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n - 1} most recent)"
|
|
692
|
+
)
|
|
653
693
|
return True
|
|
654
694
|
|
|
655
695
|
if command in ("/exit", "/quit"):
|
|
@@ -8,7 +8,7 @@ import os
|
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
10
|
from code_puppy.messaging import emit_info
|
|
11
|
-
from code_puppy.
|
|
11
|
+
from code_puppy.tui_state import is_tui_mode
|
|
12
12
|
|
|
13
13
|
from .base import MCPCommandBase
|
|
14
14
|
from .wizard_utils import run_interactive_install_wizard
|
|
@@ -130,7 +130,7 @@ class AddCommand(MCPCommandBase):
|
|
|
130
130
|
"""
|
|
131
131
|
try:
|
|
132
132
|
from code_puppy.config import MCP_SERVERS_FILE
|
|
133
|
-
from code_puppy.
|
|
133
|
+
from code_puppy.mcp_.managed_server import ServerConfig
|
|
134
134
|
|
|
135
135
|
# Extract required fields
|
|
136
136
|
name = config_dict.pop("name")
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
from typing import List, Optional
|
|
7
7
|
|
|
8
8
|
from code_puppy.messaging import emit_info
|
|
9
|
-
from code_puppy.
|
|
9
|
+
from code_puppy.tui_state import is_tui_mode
|
|
10
10
|
|
|
11
11
|
from .base import MCPCommandBase
|
|
12
12
|
from .wizard_utils import run_interactive_install_wizard
|
|
@@ -76,7 +76,7 @@ class InstallCommand(MCPCommandBase):
|
|
|
76
76
|
def _install_from_catalog(self, server_name_or_id: str, group_id: str) -> bool:
|
|
77
77
|
"""Install a server directly from the catalog by name or ID."""
|
|
78
78
|
try:
|
|
79
|
-
from code_puppy.
|
|
79
|
+
from code_puppy.mcp_.server_registry_catalog import catalog
|
|
80
80
|
from code_puppy.messaging import emit_prompt
|
|
81
81
|
|
|
82
82
|
from .utils import find_server_id_by_name
|
|
@@ -8,7 +8,7 @@ from typing import List, Optional
|
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
11
|
-
from code_puppy.
|
|
11
|
+
from code_puppy.mcp_.managed_server import ServerState
|
|
12
12
|
from code_puppy.messaging import emit_info
|
|
13
13
|
|
|
14
14
|
from .base import MCPCommandBase
|
|
@@ -34,7 +34,7 @@ class SearchCommand(MCPCommandBase):
|
|
|
34
34
|
group_id = self.generate_group_id()
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
|
-
from code_puppy.
|
|
37
|
+
from code_puppy.mcp_.server_registry_catalog import catalog
|
|
38
38
|
|
|
39
39
|
if not args:
|
|
40
40
|
# Show popular servers if no query
|
|
@@ -8,7 +8,7 @@ from typing import List, Optional
|
|
|
8
8
|
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
|
|
11
|
-
from code_puppy.
|
|
11
|
+
from code_puppy.mcp_.managed_server import ServerState
|
|
12
12
|
from code_puppy.messaging import emit_info
|
|
13
13
|
|
|
14
14
|
from .base import MCPCommandBase
|
|
@@ -117,7 +117,7 @@ class StatusCommand(MCPCommandBase):
|
|
|
117
117
|
|
|
118
118
|
# Check async lifecycle manager status if available
|
|
119
119
|
try:
|
|
120
|
-
from code_puppy.
|
|
120
|
+
from code_puppy.mcp_.async_lifecycle import get_lifecycle_manager
|
|
121
121
|
|
|
122
122
|
lifecycle_mgr = get_lifecycle_manager()
|
|
123
123
|
if lifecycle_mgr.is_running(server_id):
|
|
@@ -118,7 +118,7 @@ def interactive_server_selection(group_id: str):
|
|
|
118
118
|
# This is a simplified version - the full implementation would have
|
|
119
119
|
# category browsing, search, etc. For now, we'll just show popular servers
|
|
120
120
|
try:
|
|
121
|
-
from code_puppy.
|
|
121
|
+
from code_puppy.mcp_.server_registry_catalog import catalog
|
|
122
122
|
|
|
123
123
|
servers = catalog.get_popular(10)
|
|
124
124
|
if not servers:
|
|
@@ -256,7 +256,7 @@ def install_server_from_catalog(
|
|
|
256
256
|
import os
|
|
257
257
|
|
|
258
258
|
from code_puppy.config import MCP_SERVERS_FILE
|
|
259
|
-
from code_puppy.
|
|
259
|
+
from code_puppy.mcp_.managed_server import ServerConfig
|
|
260
260
|
|
|
261
261
|
# Set environment variables in the current environment
|
|
262
262
|
for var, value in env_vars.items():
|
code_puppy/config.py
CHANGED
|
@@ -14,6 +14,12 @@ AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
|
|
|
14
14
|
DEFAULT_SECTION = "puppy"
|
|
15
15
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
16
16
|
|
|
17
|
+
# Cache containers for model validation and defaults
|
|
18
|
+
_model_validation_cache = {}
|
|
19
|
+
_default_model_cache = None
|
|
20
|
+
_default_vision_model_cache = None
|
|
21
|
+
_default_vqa_model_cache = None
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def ensure_config_exists():
|
|
19
25
|
"""
|
|
@@ -156,9 +162,6 @@ def load_mcp_server_configs():
|
|
|
156
162
|
return {}
|
|
157
163
|
|
|
158
164
|
|
|
159
|
-
# Cache for model validation to prevent hitting ModelFactory on every call
|
|
160
|
-
_model_validation_cache = {}
|
|
161
|
-
_default_model_cache = None
|
|
162
165
|
|
|
163
166
|
|
|
164
167
|
def _default_model_from_models_json():
|
|
@@ -169,30 +172,107 @@ def _default_model_from_models_json():
|
|
|
169
172
|
"""
|
|
170
173
|
global _default_model_cache
|
|
171
174
|
|
|
172
|
-
# Return cached default if we have one
|
|
173
175
|
if _default_model_cache is not None:
|
|
174
176
|
return _default_model_cache
|
|
175
177
|
|
|
176
178
|
try:
|
|
177
|
-
# Local import to avoid potential circular dependency on module import
|
|
178
179
|
from code_puppy.model_factory import ModelFactory
|
|
179
180
|
|
|
180
181
|
models_config = ModelFactory.load_config()
|
|
181
182
|
if models_config:
|
|
182
|
-
# Get the first key from the models config
|
|
183
183
|
first_key = next(iter(models_config))
|
|
184
184
|
_default_model_cache = first_key
|
|
185
185
|
return first_key
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
_default_model_cache = "gpt-5"
|
|
189
|
-
return "gpt-5"
|
|
186
|
+
_default_model_cache = "gpt-5"
|
|
187
|
+
return "gpt-5"
|
|
190
188
|
except Exception:
|
|
191
|
-
# Any problem (network, file missing, empty dict, etc.) => fall back to gpt-5
|
|
192
189
|
_default_model_cache = "gpt-5"
|
|
193
190
|
return "gpt-5"
|
|
194
191
|
|
|
195
192
|
|
|
193
|
+
def _default_vision_model_from_models_json() -> str:
|
|
194
|
+
"""Select a default vision-capable model from models.json with caching."""
|
|
195
|
+
global _default_vision_model_cache
|
|
196
|
+
|
|
197
|
+
if _default_vision_model_cache is not None:
|
|
198
|
+
return _default_vision_model_cache
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
from code_puppy.model_factory import ModelFactory
|
|
202
|
+
|
|
203
|
+
models_config = ModelFactory.load_config()
|
|
204
|
+
if models_config:
|
|
205
|
+
# Prefer explicitly tagged vision models
|
|
206
|
+
for name, config in models_config.items():
|
|
207
|
+
if config.get("supports_vision"):
|
|
208
|
+
_default_vision_model_cache = name
|
|
209
|
+
return name
|
|
210
|
+
|
|
211
|
+
# Fallback heuristic: common multimodal models
|
|
212
|
+
preferred_candidates = (
|
|
213
|
+
"gpt-4.1",
|
|
214
|
+
"gpt-4.1-mini",
|
|
215
|
+
"gpt-4.1-nano",
|
|
216
|
+
"claude-4-0-sonnet",
|
|
217
|
+
"gemini-2.5-flash-preview-05-20",
|
|
218
|
+
)
|
|
219
|
+
for candidate in preferred_candidates:
|
|
220
|
+
if candidate in models_config:
|
|
221
|
+
_default_vision_model_cache = candidate
|
|
222
|
+
return candidate
|
|
223
|
+
|
|
224
|
+
# Last resort: use the general default model
|
|
225
|
+
_default_vision_model_cache = _default_model_from_models_json()
|
|
226
|
+
return _default_vision_model_cache
|
|
227
|
+
|
|
228
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
229
|
+
return "gpt-4.1"
|
|
230
|
+
except Exception:
|
|
231
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
232
|
+
return "gpt-4.1"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _default_vqa_model_from_models_json() -> str:
|
|
236
|
+
"""Select a default VQA-capable model, preferring vision-ready options."""
|
|
237
|
+
global _default_vqa_model_cache
|
|
238
|
+
|
|
239
|
+
if _default_vqa_model_cache is not None:
|
|
240
|
+
return _default_vqa_model_cache
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
from code_puppy.model_factory import ModelFactory
|
|
244
|
+
|
|
245
|
+
models_config = ModelFactory.load_config()
|
|
246
|
+
if models_config:
|
|
247
|
+
# Allow explicit VQA hints if present
|
|
248
|
+
for name, config in models_config.items():
|
|
249
|
+
if config.get("supports_vqa"):
|
|
250
|
+
_default_vqa_model_cache = name
|
|
251
|
+
return name
|
|
252
|
+
|
|
253
|
+
# Reuse multimodal heuristics before falling back to generic default
|
|
254
|
+
preferred_candidates = (
|
|
255
|
+
"gpt-4.1",
|
|
256
|
+
"gpt-4.1-mini",
|
|
257
|
+
"claude-4-0-sonnet",
|
|
258
|
+
"gemini-2.5-flash-preview-05-20",
|
|
259
|
+
"gpt-4.1-nano",
|
|
260
|
+
)
|
|
261
|
+
for candidate in preferred_candidates:
|
|
262
|
+
if candidate in models_config:
|
|
263
|
+
_default_vqa_model_cache = candidate
|
|
264
|
+
return candidate
|
|
265
|
+
|
|
266
|
+
_default_vqa_model_cache = _default_model_from_models_json()
|
|
267
|
+
return _default_vqa_model_cache
|
|
268
|
+
|
|
269
|
+
_default_vqa_model_cache = "gpt-4.1"
|
|
270
|
+
return "gpt-4.1"
|
|
271
|
+
except Exception:
|
|
272
|
+
_default_vqa_model_cache = "gpt-4.1"
|
|
273
|
+
return "gpt-4.1"
|
|
274
|
+
|
|
275
|
+
|
|
196
276
|
def _validate_model_exists(model_name: str) -> bool:
|
|
197
277
|
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
198
278
|
global _model_validation_cache
|
|
@@ -218,9 +298,11 @@ def _validate_model_exists(model_name: str) -> bool:
|
|
|
218
298
|
|
|
219
299
|
def clear_model_cache():
|
|
220
300
|
"""Clear the model validation cache. Call this when models.json changes."""
|
|
221
|
-
global _model_validation_cache, _default_model_cache
|
|
301
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache, _default_vqa_model_cache
|
|
222
302
|
_model_validation_cache.clear()
|
|
223
303
|
_default_model_cache = None
|
|
304
|
+
_default_vision_model_cache = None
|
|
305
|
+
_default_vqa_model_cache = None
|
|
224
306
|
|
|
225
307
|
|
|
226
308
|
def get_model_name():
|
|
@@ -258,6 +340,20 @@ def set_model_name(model: str):
|
|
|
258
340
|
clear_model_cache()
|
|
259
341
|
|
|
260
342
|
|
|
343
|
+
def get_vqa_model_name() -> str:
|
|
344
|
+
"""Return the configured VQA model, falling back to an inferred default."""
|
|
345
|
+
stored_model = get_value("vqa_model_name")
|
|
346
|
+
if stored_model and _validate_model_exists(stored_model):
|
|
347
|
+
return stored_model
|
|
348
|
+
return _default_vqa_model_from_models_json()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def set_vqa_model_name(model: str):
|
|
352
|
+
"""Persist the configured VQA model name and refresh caches."""
|
|
353
|
+
set_config_value("vqa_model_name", model or "")
|
|
354
|
+
clear_model_cache()
|
|
355
|
+
|
|
356
|
+
|
|
261
357
|
def get_puppy_token():
|
|
262
358
|
"""Returns the puppy_token from config, or None if not set."""
|
|
263
359
|
return get_value("puppy_token")
|
|
@@ -493,3 +589,36 @@ def save_command_to_history(command: str):
|
|
|
493
589
|
f"❌ An unexpected error occurred while saving command history: {str(e)}"
|
|
494
590
|
)
|
|
495
591
|
direct_console.print(f"[bold red]{error_msg}[/bold red]")
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def get_agent_pinned_model(agent_name: str) -> str:
|
|
595
|
+
"""Get the pinned model for a specific agent.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
agent_name: Name of the agent to get the pinned model for.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Pinned model name, or None if no model is pinned for this agent.
|
|
602
|
+
"""
|
|
603
|
+
return get_value(f"agent_model_{agent_name}")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def set_agent_pinned_model(agent_name: str, model_name: str):
|
|
607
|
+
"""Set the pinned model for a specific agent.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
agent_name: Name of the agent to pin the model for.
|
|
611
|
+
model_name: Model name to pin to this agent.
|
|
612
|
+
"""
|
|
613
|
+
set_config_value(f"agent_model_{agent_name}", model_name)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def clear_agent_pinned_model(agent_name: str):
|
|
617
|
+
"""Clear the pinned model for a specific agent.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
agent_name: Name of the agent to clear the pinned model for.
|
|
621
|
+
"""
|
|
622
|
+
# We can't easily delete keys from configparser, so set to empty string
|
|
623
|
+
# which will be treated as None by get_agent_pinned_model
|
|
624
|
+
set_config_value(f"agent_model_{agent_name}", "")
|
code_puppy/http_utils.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Dict, Optional, Union
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
import requests
|
|
13
|
-
from tenacity import
|
|
13
|
+
from tenacity import stop_after_attempt, wait_exponential
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
from pydantic_ai.retries import (
|
|
@@ -57,26 +57,32 @@ def create_client(
|
|
|
57
57
|
|
|
58
58
|
# If retry components are available, create a client with retry transport
|
|
59
59
|
if TenacityTransport and RetryConfig and wait_retry_after:
|
|
60
|
+
|
|
60
61
|
def should_retry_status(response):
|
|
61
62
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
62
63
|
if response.status_code in retry_status_codes:
|
|
63
|
-
emit_info(
|
|
64
|
+
emit_info(
|
|
65
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
66
|
+
)
|
|
64
67
|
response.raise_for_status()
|
|
65
68
|
|
|
66
69
|
transport = TenacityTransport(
|
|
67
70
|
config=RetryConfig(
|
|
68
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
71
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
72
|
+
and e.response.status_code in retry_status_codes,
|
|
69
73
|
wait=wait_retry_after(
|
|
70
74
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
71
|
-
max_wait=300
|
|
75
|
+
max_wait=300,
|
|
72
76
|
),
|
|
73
77
|
stop=stop_after_attempt(10),
|
|
74
|
-
reraise=True
|
|
78
|
+
reraise=True,
|
|
75
79
|
),
|
|
76
|
-
validate_response=should_retry_status
|
|
80
|
+
validate_response=should_retry_status,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return httpx.Client(
|
|
84
|
+
transport=transport, verify=verify, headers=headers or {}, timeout=timeout
|
|
77
85
|
)
|
|
78
|
-
|
|
79
|
-
return httpx.Client(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
80
86
|
else:
|
|
81
87
|
# Fallback to regular client if retry components are not available
|
|
82
88
|
return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
|
|
@@ -93,26 +99,32 @@ def create_async_client(
|
|
|
93
99
|
|
|
94
100
|
# If retry components are available, create a client with retry transport
|
|
95
101
|
if AsyncTenacityTransport and RetryConfig and wait_retry_after:
|
|
102
|
+
|
|
96
103
|
def should_retry_status(response):
|
|
97
104
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
98
105
|
if response.status_code in retry_status_codes:
|
|
99
|
-
emit_info(
|
|
106
|
+
emit_info(
|
|
107
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
108
|
+
)
|
|
100
109
|
response.raise_for_status()
|
|
101
110
|
|
|
102
111
|
transport = AsyncTenacityTransport(
|
|
103
112
|
config=RetryConfig(
|
|
104
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
113
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
114
|
+
and e.response.status_code in retry_status_codes,
|
|
105
115
|
wait=wait_retry_after(
|
|
106
116
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
107
|
-
max_wait=300
|
|
117
|
+
max_wait=300,
|
|
108
118
|
),
|
|
109
119
|
stop=stop_after_attempt(10),
|
|
110
|
-
reraise=True
|
|
120
|
+
reraise=True,
|
|
111
121
|
),
|
|
112
|
-
validate_response=should_retry_status
|
|
122
|
+
validate_response=should_retry_status,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return httpx.AsyncClient(
|
|
126
|
+
transport=transport, verify=verify, headers=headers or {}, timeout=timeout
|
|
113
127
|
)
|
|
114
|
-
|
|
115
|
-
return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
116
128
|
else:
|
|
117
129
|
# Fallback to regular client if retry components are not available
|
|
118
130
|
return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
|
|
@@ -169,32 +181,44 @@ def create_reopenable_async_client(
|
|
|
169
181
|
|
|
170
182
|
# If retry components are available, create a client with retry transport
|
|
171
183
|
if AsyncTenacityTransport and RetryConfig and wait_retry_after:
|
|
184
|
+
|
|
172
185
|
def should_retry_status(response):
|
|
173
186
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
174
187
|
if response.status_code in retry_status_codes:
|
|
175
|
-
emit_info(
|
|
188
|
+
emit_info(
|
|
189
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
190
|
+
)
|
|
176
191
|
response.raise_for_status()
|
|
177
192
|
|
|
178
193
|
transport = AsyncTenacityTransport(
|
|
179
194
|
config=RetryConfig(
|
|
180
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
195
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
196
|
+
and e.response.status_code in retry_status_codes,
|
|
181
197
|
wait=wait_retry_after(
|
|
182
198
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
183
|
-
max_wait=300
|
|
199
|
+
max_wait=300,
|
|
184
200
|
),
|
|
185
201
|
stop=stop_after_attempt(10),
|
|
186
|
-
reraise=True
|
|
202
|
+
reraise=True,
|
|
187
203
|
),
|
|
188
|
-
validate_response=should_retry_status
|
|
204
|
+
validate_response=should_retry_status,
|
|
189
205
|
)
|
|
190
|
-
|
|
206
|
+
|
|
191
207
|
if ReopenableAsyncClient is not None:
|
|
192
208
|
return ReopenableAsyncClient(
|
|
193
|
-
transport=transport,
|
|
209
|
+
transport=transport,
|
|
210
|
+
verify=verify,
|
|
211
|
+
headers=headers or {},
|
|
212
|
+
timeout=timeout,
|
|
194
213
|
)
|
|
195
214
|
else:
|
|
196
215
|
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
197
|
-
return httpx.AsyncClient(
|
|
216
|
+
return httpx.AsyncClient(
|
|
217
|
+
transport=transport,
|
|
218
|
+
verify=verify,
|
|
219
|
+
headers=headers or {},
|
|
220
|
+
timeout=timeout,
|
|
221
|
+
)
|
|
198
222
|
else:
|
|
199
223
|
# Fallback to regular clients if retry components are not available
|
|
200
224
|
if ReopenableAsyncClient is not None:
|
|
@@ -203,7 +227,9 @@ def create_reopenable_async_client(
|
|
|
203
227
|
)
|
|
204
228
|
else:
|
|
205
229
|
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
206
|
-
return httpx.AsyncClient(
|
|
230
|
+
return httpx.AsyncClient(
|
|
231
|
+
verify=verify, headers=headers or {}, timeout=timeout
|
|
232
|
+
)
|
|
207
233
|
|
|
208
234
|
|
|
209
235
|
def is_cert_bundle_available() -> bool:
|
code_puppy/main.py
CHANGED
|
@@ -29,7 +29,8 @@ from code_puppy.message_history_processor import (
|
|
|
29
29
|
message_history_accumulator,
|
|
30
30
|
prune_interrupted_tool_calls,
|
|
31
31
|
)
|
|
32
|
-
from code_puppy.state_management import
|
|
32
|
+
from code_puppy.state_management import set_message_history
|
|
33
|
+
from code_puppy.tui_state import is_tui_mode, set_tui_mode
|
|
33
34
|
from code_puppy.tools.common import console
|
|
34
35
|
from code_puppy.version_checker import default_version_mismatch_behavior
|
|
35
36
|
|