superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Apply Patch Tool for Open Responses.
|
|
3
|
+
|
|
4
|
+
Implements the apply_patch built-in tool for applying unified diff patches
|
|
5
|
+
to files in the workspace. Critical for QIR (Quality Issue Resolution) fixes.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Dry-run mode by default for safety
|
|
9
|
+
- Git-based patch application
|
|
10
|
+
- Validation before application
|
|
11
|
+
- Detailed error reporting
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
tool = ApplyPatchTool(workspace_root="/path/to/project")
|
|
15
|
+
|
|
16
|
+
# Validate without applying
|
|
17
|
+
result = await tool.execute(patch_content, dry_run=True)
|
|
18
|
+
|
|
19
|
+
# Apply the patch
|
|
20
|
+
result = await tool.execute(patch_content, dry_run=False)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import os
|
|
27
|
+
import tempfile
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PatchResult:
|
|
35
|
+
"""Result of a patch operation."""
|
|
36
|
+
|
|
37
|
+
success: bool
|
|
38
|
+
message: str
|
|
39
|
+
files_modified: List[str] = field(default_factory=list)
|
|
40
|
+
errors: List[str] = field(default_factory=list)
|
|
41
|
+
dry_run: bool = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class PatchOperation:
|
|
46
|
+
"""A single file operation from a patch."""
|
|
47
|
+
|
|
48
|
+
operation: str # "create", "update", "delete"
|
|
49
|
+
path: str
|
|
50
|
+
old_path: Optional[str] = None # For renames
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ApplyPatchTool:
|
|
54
|
+
"""
|
|
55
|
+
Apply patch tool for Open Responses.
|
|
56
|
+
|
|
57
|
+
Applies unified diff patches to files in the workspace.
|
|
58
|
+
Uses git apply for robust patch handling.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
workspace_root: Root directory for file operations
|
|
62
|
+
dry_run: If True (default), validate without applying
|
|
63
|
+
allow_outside_workspace: If True, allow patches to files outside workspace
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
workspace_root: str,
|
|
69
|
+
dry_run: bool = True,
|
|
70
|
+
allow_outside_workspace: bool = False,
|
|
71
|
+
):
|
|
72
|
+
self.workspace_root = Path(workspace_root).resolve()
|
|
73
|
+
self.dry_run = dry_run
|
|
74
|
+
self.allow_outside_workspace = allow_outside_workspace
|
|
75
|
+
|
|
76
|
+
async def execute(
|
|
77
|
+
self,
|
|
78
|
+
patch: str,
|
|
79
|
+
dry_run: Optional[bool] = None,
|
|
80
|
+
) -> Dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
Execute the patch operation.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
patch: The patch content in unified diff format
|
|
86
|
+
dry_run: Override the default dry_run setting
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict with success status, message, and details
|
|
90
|
+
"""
|
|
91
|
+
use_dry_run = dry_run if dry_run is not None else self.dry_run
|
|
92
|
+
|
|
93
|
+
# Parse patch to extract operations
|
|
94
|
+
operations = self._parse_patch(patch)
|
|
95
|
+
|
|
96
|
+
if not operations:
|
|
97
|
+
return {
|
|
98
|
+
"success": False,
|
|
99
|
+
"message": "No valid patch operations found",
|
|
100
|
+
"dry_run": use_dry_run,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Validate paths
|
|
104
|
+
validation_errors = self._validate_paths(operations)
|
|
105
|
+
if validation_errors:
|
|
106
|
+
return {
|
|
107
|
+
"success": False,
|
|
108
|
+
"message": "Path validation failed",
|
|
109
|
+
"errors": validation_errors,
|
|
110
|
+
"dry_run": use_dry_run,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Apply or validate the patch
|
|
114
|
+
if use_dry_run:
|
|
115
|
+
result = await self._validate_patch(patch)
|
|
116
|
+
else:
|
|
117
|
+
result = await self._apply_patch(patch)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"success": result.success,
|
|
121
|
+
"message": result.message,
|
|
122
|
+
"files_modified": result.files_modified,
|
|
123
|
+
"errors": result.errors,
|
|
124
|
+
"dry_run": use_dry_run,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def _parse_patch(self, patch: str) -> List[PatchOperation]:
|
|
128
|
+
"""Parse a patch to extract file operations."""
|
|
129
|
+
operations = []
|
|
130
|
+
lines = patch.split("\n")
|
|
131
|
+
current_file = None
|
|
132
|
+
is_new_file = False
|
|
133
|
+
is_delete = False
|
|
134
|
+
|
|
135
|
+
for line in lines:
|
|
136
|
+
if line.startswith("diff --git"):
|
|
137
|
+
# New file in patch
|
|
138
|
+
parts = line.split()
|
|
139
|
+
if len(parts) >= 4:
|
|
140
|
+
# Extract paths: diff --git a/path b/path
|
|
141
|
+
a_path = parts[2][2:] if parts[2].startswith("a/") else parts[2]
|
|
142
|
+
b_path = parts[3][2:] if parts[3].startswith("b/") else parts[3]
|
|
143
|
+
current_file = b_path if b_path != "/dev/null" else a_path
|
|
144
|
+
is_new_file = False
|
|
145
|
+
is_delete = False
|
|
146
|
+
|
|
147
|
+
elif line.startswith("new file mode"):
|
|
148
|
+
is_new_file = True
|
|
149
|
+
|
|
150
|
+
elif line.startswith("deleted file mode"):
|
|
151
|
+
is_delete = True
|
|
152
|
+
|
|
153
|
+
elif line.startswith("--- ") and current_file:
|
|
154
|
+
old_path = line[4:].strip()
|
|
155
|
+
if old_path.startswith("a/"):
|
|
156
|
+
old_path = old_path[2:]
|
|
157
|
+
elif old_path == "/dev/null":
|
|
158
|
+
is_new_file = True
|
|
159
|
+
|
|
160
|
+
elif line.startswith("+++ ") and current_file:
|
|
161
|
+
new_path = line[4:].strip()
|
|
162
|
+
if new_path.startswith("b/"):
|
|
163
|
+
new_path = new_path[2:]
|
|
164
|
+
elif new_path == "/dev/null":
|
|
165
|
+
is_delete = True
|
|
166
|
+
|
|
167
|
+
# Determine operation type
|
|
168
|
+
if is_delete:
|
|
169
|
+
op_type = "delete"
|
|
170
|
+
elif is_new_file:
|
|
171
|
+
op_type = "create"
|
|
172
|
+
else:
|
|
173
|
+
op_type = "update"
|
|
174
|
+
|
|
175
|
+
operations.append(
|
|
176
|
+
PatchOperation(
|
|
177
|
+
operation=op_type,
|
|
178
|
+
path=current_file,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
current_file = None
|
|
182
|
+
|
|
183
|
+
return operations
|
|
184
|
+
|
|
185
|
+
def _validate_paths(self, operations: List[PatchOperation]) -> List[str]:
|
|
186
|
+
"""Validate that all paths are within the workspace."""
|
|
187
|
+
errors = []
|
|
188
|
+
|
|
189
|
+
if self.allow_outside_workspace:
|
|
190
|
+
return errors
|
|
191
|
+
|
|
192
|
+
for op in operations:
|
|
193
|
+
try:
|
|
194
|
+
full_path = (self.workspace_root / op.path).resolve()
|
|
195
|
+
if not str(full_path).startswith(str(self.workspace_root)):
|
|
196
|
+
errors.append(f"Path '{op.path}' is outside workspace")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
errors.append(f"Invalid path '{op.path}': {e}")
|
|
199
|
+
|
|
200
|
+
return errors
|
|
201
|
+
|
|
202
|
+
async def _validate_patch(self, patch: str) -> PatchResult:
|
|
203
|
+
"""Validate a patch without applying it."""
|
|
204
|
+
# Write patch to temporary file
|
|
205
|
+
with tempfile.NamedTemporaryFile(
|
|
206
|
+
mode="w",
|
|
207
|
+
suffix=".patch",
|
|
208
|
+
delete=False,
|
|
209
|
+
) as f:
|
|
210
|
+
f.write(patch)
|
|
211
|
+
patch_file = f.name
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Run git apply --check
|
|
215
|
+
proc = await asyncio.create_subprocess_exec(
|
|
216
|
+
"git",
|
|
217
|
+
"apply",
|
|
218
|
+
"--check",
|
|
219
|
+
patch_file,
|
|
220
|
+
cwd=str(self.workspace_root),
|
|
221
|
+
stdout=asyncio.subprocess.PIPE,
|
|
222
|
+
stderr=asyncio.subprocess.PIPE,
|
|
223
|
+
)
|
|
224
|
+
stdout, stderr = await proc.communicate()
|
|
225
|
+
|
|
226
|
+
if proc.returncode == 0:
|
|
227
|
+
# Parse operations for file list
|
|
228
|
+
operations = self._parse_patch(patch)
|
|
229
|
+
return PatchResult(
|
|
230
|
+
success=True,
|
|
231
|
+
message="Patch validation successful",
|
|
232
|
+
files_modified=[op.path for op in operations],
|
|
233
|
+
dry_run=True,
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
error_msg = stderr.decode("utf-8").strip()
|
|
237
|
+
return PatchResult(
|
|
238
|
+
success=False,
|
|
239
|
+
message="Patch validation failed",
|
|
240
|
+
errors=[error_msg] if error_msg else ["Patch does not apply cleanly"],
|
|
241
|
+
dry_run=True,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
except FileNotFoundError:
|
|
245
|
+
# Git not available, try manual validation
|
|
246
|
+
return await self._validate_patch_manual(patch)
|
|
247
|
+
finally:
|
|
248
|
+
# Clean up temp file
|
|
249
|
+
try:
|
|
250
|
+
os.unlink(patch_file)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
async def _validate_patch_manual(self, patch: str) -> PatchResult:
|
|
255
|
+
"""Manually validate a patch without git."""
|
|
256
|
+
operations = self._parse_patch(patch)
|
|
257
|
+
errors = []
|
|
258
|
+
|
|
259
|
+
for op in operations:
|
|
260
|
+
full_path = self.workspace_root / op.path
|
|
261
|
+
|
|
262
|
+
if op.operation == "update":
|
|
263
|
+
if not full_path.exists():
|
|
264
|
+
errors.append(f"File does not exist: {op.path}")
|
|
265
|
+
|
|
266
|
+
elif op.operation == "create":
|
|
267
|
+
if full_path.exists():
|
|
268
|
+
errors.append(f"File already exists: {op.path}")
|
|
269
|
+
|
|
270
|
+
elif op.operation == "delete":
|
|
271
|
+
if not full_path.exists():
|
|
272
|
+
errors.append(f"File does not exist: {op.path}")
|
|
273
|
+
|
|
274
|
+
if errors:
|
|
275
|
+
return PatchResult(
|
|
276
|
+
success=False,
|
|
277
|
+
message="Patch validation failed",
|
|
278
|
+
errors=errors,
|
|
279
|
+
dry_run=True,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return PatchResult(
|
|
283
|
+
success=True,
|
|
284
|
+
message="Patch validation successful (manual check)",
|
|
285
|
+
files_modified=[op.path for op in operations],
|
|
286
|
+
dry_run=True,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def _apply_patch(self, patch: str) -> PatchResult:
|
|
290
|
+
"""Apply a patch to the workspace."""
|
|
291
|
+
# Write patch to temporary file
|
|
292
|
+
with tempfile.NamedTemporaryFile(
|
|
293
|
+
mode="w",
|
|
294
|
+
suffix=".patch",
|
|
295
|
+
delete=False,
|
|
296
|
+
) as f:
|
|
297
|
+
f.write(patch)
|
|
298
|
+
patch_file = f.name
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Run git apply
|
|
302
|
+
proc = await asyncio.create_subprocess_exec(
|
|
303
|
+
"git",
|
|
304
|
+
"apply",
|
|
305
|
+
patch_file,
|
|
306
|
+
cwd=str(self.workspace_root),
|
|
307
|
+
stdout=asyncio.subprocess.PIPE,
|
|
308
|
+
stderr=asyncio.subprocess.PIPE,
|
|
309
|
+
)
|
|
310
|
+
stdout, stderr = await proc.communicate()
|
|
311
|
+
|
|
312
|
+
if proc.returncode == 0:
|
|
313
|
+
operations = self._parse_patch(patch)
|
|
314
|
+
return PatchResult(
|
|
315
|
+
success=True,
|
|
316
|
+
message="Patch applied successfully",
|
|
317
|
+
files_modified=[op.path for op in operations],
|
|
318
|
+
dry_run=False,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
error_msg = stderr.decode("utf-8").strip()
|
|
322
|
+
return PatchResult(
|
|
323
|
+
success=False,
|
|
324
|
+
message="Failed to apply patch",
|
|
325
|
+
errors=[error_msg] if error_msg else ["Patch application failed"],
|
|
326
|
+
dry_run=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
except FileNotFoundError:
|
|
330
|
+
# Git not available, try manual application
|
|
331
|
+
return await self._apply_patch_manual(patch)
|
|
332
|
+
finally:
|
|
333
|
+
# Clean up temp file
|
|
334
|
+
try:
|
|
335
|
+
os.unlink(patch_file)
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
async def _apply_patch_manual(self, patch: str) -> PatchResult:
|
|
340
|
+
"""Manually apply a patch without git (limited support)."""
|
|
341
|
+
return PatchResult(
|
|
342
|
+
success=False,
|
|
343
|
+
message="Manual patch application not supported. Please install git.",
|
|
344
|
+
errors=["Git is required for patch application"],
|
|
345
|
+
dry_run=False,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def get_tool_definition(self) -> Dict[str, Any]:
|
|
349
|
+
"""Get the Open Responses tool definition for apply_patch."""
|
|
350
|
+
return {
|
|
351
|
+
"type": "apply_patch",
|
|
352
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Interpreter Tool for Open Responses.
|
|
3
|
+
|
|
4
|
+
Executes code in a sandboxed environment. Supports:
|
|
5
|
+
- Python execution
|
|
6
|
+
- Shell command execution
|
|
7
|
+
- Test running
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Timeout control
|
|
11
|
+
- Output capture
|
|
12
|
+
- Error handling
|
|
13
|
+
- Resource limits
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
tool = CodeInterpreterTool(workspace_root="/path/to/project")
|
|
17
|
+
result = await tool.execute("print('Hello, World!')", language="python")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
import tempfile
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ExecutionResult:
|
|
32
|
+
"""Result of code execution."""
|
|
33
|
+
|
|
34
|
+
success: bool
|
|
35
|
+
output: str
|
|
36
|
+
error: str = ""
|
|
37
|
+
exit_code: int = 0
|
|
38
|
+
timed_out: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CodeInterpreterTool:
|
|
42
|
+
"""
|
|
43
|
+
Code interpreter tool for Open Responses.
|
|
44
|
+
|
|
45
|
+
Executes code in a controlled environment with timeout
|
|
46
|
+
and output capture.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
workspace_root: Root directory for execution
|
|
50
|
+
timeout: Maximum execution time in seconds
|
|
51
|
+
max_output_size: Maximum output size in bytes
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
workspace_root: str,
|
|
57
|
+
timeout: float = 60.0,
|
|
58
|
+
max_output_size: int = 100 * 1024, # 100KB
|
|
59
|
+
):
|
|
60
|
+
self.workspace_root = Path(workspace_root).resolve()
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self.max_output_size = max_output_size
|
|
63
|
+
|
|
64
|
+
async def execute(
|
|
65
|
+
self,
|
|
66
|
+
code: str,
|
|
67
|
+
language: str = "python",
|
|
68
|
+
timeout: Optional[float] = None,
|
|
69
|
+
) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Execute code in the specified language.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
code: The code to execute
|
|
75
|
+
language: Programming language ("python", "shell", "bash")
|
|
76
|
+
timeout: Override default timeout
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dict with success status, output, and details
|
|
80
|
+
"""
|
|
81
|
+
use_timeout = timeout if timeout is not None else self.timeout
|
|
82
|
+
|
|
83
|
+
if language in ("python", "python3"):
|
|
84
|
+
result = await self._execute_python(code, use_timeout)
|
|
85
|
+
elif language in ("shell", "bash", "sh"):
|
|
86
|
+
result = await self._execute_shell(code, use_timeout)
|
|
87
|
+
else:
|
|
88
|
+
return {
|
|
89
|
+
"success": False,
|
|
90
|
+
"output": "",
|
|
91
|
+
"error": f"Unsupported language: {language}",
|
|
92
|
+
"exit_code": 1,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"success": result.success,
|
|
97
|
+
"output": result.output,
|
|
98
|
+
"error": result.error,
|
|
99
|
+
"exit_code": result.exit_code,
|
|
100
|
+
"timed_out": result.timed_out,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async def run_tests(
|
|
104
|
+
self,
|
|
105
|
+
test_command: Optional[str] = None,
|
|
106
|
+
test_file: Optional[str] = None,
|
|
107
|
+
timeout: Optional[float] = None,
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Run tests in the workspace.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
test_command: Custom test command
|
|
114
|
+
test_file: Specific test file to run
|
|
115
|
+
timeout: Override default timeout
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with test results
|
|
119
|
+
"""
|
|
120
|
+
use_timeout = timeout if timeout is not None else self.timeout
|
|
121
|
+
|
|
122
|
+
# Determine test command
|
|
123
|
+
if test_command:
|
|
124
|
+
cmd = test_command
|
|
125
|
+
elif test_file:
|
|
126
|
+
cmd = f"python -m pytest {test_file} -v"
|
|
127
|
+
else:
|
|
128
|
+
# Auto-detect test framework
|
|
129
|
+
cmd = await self._detect_test_command()
|
|
130
|
+
|
|
131
|
+
result = await self._execute_shell(cmd, use_timeout)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"success": result.success,
|
|
135
|
+
"output": result.output,
|
|
136
|
+
"error": result.error,
|
|
137
|
+
"exit_code": result.exit_code,
|
|
138
|
+
"timed_out": result.timed_out,
|
|
139
|
+
"command": cmd,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async def _execute_python(
|
|
143
|
+
self,
|
|
144
|
+
code: str,
|
|
145
|
+
timeout: float,
|
|
146
|
+
) -> ExecutionResult:
|
|
147
|
+
"""Execute Python code."""
|
|
148
|
+
# Write code to temporary file
|
|
149
|
+
with tempfile.NamedTemporaryFile(
|
|
150
|
+
mode="w",
|
|
151
|
+
suffix=".py",
|
|
152
|
+
delete=False,
|
|
153
|
+
dir=str(self.workspace_root),
|
|
154
|
+
) as f:
|
|
155
|
+
f.write(code)
|
|
156
|
+
script_file = f.name
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
proc = await asyncio.create_subprocess_exec(
|
|
160
|
+
"python3",
|
|
161
|
+
script_file,
|
|
162
|
+
cwd=str(self.workspace_root),
|
|
163
|
+
stdout=asyncio.subprocess.PIPE,
|
|
164
|
+
stderr=asyncio.subprocess.PIPE,
|
|
165
|
+
env={**os.environ, "PYTHONUNBUFFERED": "1"},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
stdout, stderr = await asyncio.wait_for(
|
|
170
|
+
proc.communicate(),
|
|
171
|
+
timeout=timeout,
|
|
172
|
+
)
|
|
173
|
+
timed_out = False
|
|
174
|
+
except asyncio.TimeoutError:
|
|
175
|
+
proc.kill()
|
|
176
|
+
await proc.wait()
|
|
177
|
+
return ExecutionResult(
|
|
178
|
+
success=False,
|
|
179
|
+
output="",
|
|
180
|
+
error=f"Execution timed out after {timeout} seconds",
|
|
181
|
+
exit_code=-1,
|
|
182
|
+
timed_out=True,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
output = stdout.decode("utf-8", errors="replace")
|
|
186
|
+
error = stderr.decode("utf-8", errors="replace")
|
|
187
|
+
|
|
188
|
+
# Truncate if needed
|
|
189
|
+
if len(output) > self.max_output_size:
|
|
190
|
+
output = output[: self.max_output_size] + "\n[Output truncated]"
|
|
191
|
+
if len(error) > self.max_output_size:
|
|
192
|
+
error = error[: self.max_output_size] + "\n[Error output truncated]"
|
|
193
|
+
|
|
194
|
+
return ExecutionResult(
|
|
195
|
+
success=proc.returncode == 0,
|
|
196
|
+
output=output,
|
|
197
|
+
error=error,
|
|
198
|
+
exit_code=proc.returncode or 0,
|
|
199
|
+
timed_out=False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
finally:
|
|
203
|
+
# Clean up temp file
|
|
204
|
+
try:
|
|
205
|
+
os.unlink(script_file)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
async def _execute_shell(
|
|
210
|
+
self,
|
|
211
|
+
command: str,
|
|
212
|
+
timeout: float,
|
|
213
|
+
) -> ExecutionResult:
|
|
214
|
+
"""Execute a shell command."""
|
|
215
|
+
try:
|
|
216
|
+
proc = await asyncio.create_subprocess_shell(
|
|
217
|
+
command,
|
|
218
|
+
cwd=str(self.workspace_root),
|
|
219
|
+
stdout=asyncio.subprocess.PIPE,
|
|
220
|
+
stderr=asyncio.subprocess.PIPE,
|
|
221
|
+
env=os.environ.copy(),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
stdout, stderr = await asyncio.wait_for(
|
|
226
|
+
proc.communicate(),
|
|
227
|
+
timeout=timeout,
|
|
228
|
+
)
|
|
229
|
+
timed_out = False
|
|
230
|
+
except asyncio.TimeoutError:
|
|
231
|
+
proc.kill()
|
|
232
|
+
await proc.wait()
|
|
233
|
+
return ExecutionResult(
|
|
234
|
+
success=False,
|
|
235
|
+
output="",
|
|
236
|
+
error=f"Execution timed out after {timeout} seconds",
|
|
237
|
+
exit_code=-1,
|
|
238
|
+
timed_out=True,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
output = stdout.decode("utf-8", errors="replace")
|
|
242
|
+
error = stderr.decode("utf-8", errors="replace")
|
|
243
|
+
|
|
244
|
+
# Truncate if needed
|
|
245
|
+
if len(output) > self.max_output_size:
|
|
246
|
+
output = output[: self.max_output_size] + "\n[Output truncated]"
|
|
247
|
+
if len(error) > self.max_output_size:
|
|
248
|
+
error = error[: self.max_output_size] + "\n[Error output truncated]"
|
|
249
|
+
|
|
250
|
+
return ExecutionResult(
|
|
251
|
+
success=proc.returncode == 0,
|
|
252
|
+
output=output,
|
|
253
|
+
error=error,
|
|
254
|
+
exit_code=proc.returncode or 0,
|
|
255
|
+
timed_out=False,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
return ExecutionResult(
|
|
260
|
+
success=False,
|
|
261
|
+
output="",
|
|
262
|
+
error=str(e),
|
|
263
|
+
exit_code=1,
|
|
264
|
+
timed_out=False,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
async def _detect_test_command(self) -> str:
|
|
268
|
+
"""Detect the appropriate test command for the workspace."""
|
|
269
|
+
# Check for common test configurations
|
|
270
|
+
if (self.workspace_root / "pytest.ini").exists():
|
|
271
|
+
return "python -m pytest -v"
|
|
272
|
+
if (self.workspace_root / "pyproject.toml").exists():
|
|
273
|
+
return "python -m pytest -v"
|
|
274
|
+
if (self.workspace_root / "setup.py").exists():
|
|
275
|
+
return "python -m pytest -v"
|
|
276
|
+
if (self.workspace_root / "package.json").exists():
|
|
277
|
+
return "npm test"
|
|
278
|
+
if (self.workspace_root / "Cargo.toml").exists():
|
|
279
|
+
return "cargo test"
|
|
280
|
+
if (self.workspace_root / "go.mod").exists():
|
|
281
|
+
return "go test ./..."
|
|
282
|
+
|
|
283
|
+
# Default to pytest
|
|
284
|
+
return "python -m pytest -v"
|
|
285
|
+
|
|
286
|
+
def get_tool_definition(self) -> Dict[str, Any]:
|
|
287
|
+
"""Get the Open Responses tool definition for code_interpreter."""
|
|
288
|
+
return {
|
|
289
|
+
"type": "code_interpreter",
|
|
290
|
+
}
|