morphsdk 0.2.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.
- morphsdk/__init__.py +54 -0
- morphsdk/_agent/__init__.py +64 -0
- morphsdk/_agent/config.py +52 -0
- morphsdk/_agent/explore.py +276 -0
- morphsdk/_agent/github.py +57 -0
- morphsdk/_agent/helpers.py +133 -0
- morphsdk/_agent/parser.py +163 -0
- morphsdk/_agent/runner.py +524 -0
- morphsdk/_agent/tools.py +171 -0
- morphsdk/_agent/types.py +126 -0
- morphsdk/_base.py +309 -0
- morphsdk/_client.py +245 -0
- morphsdk/_config.py +37 -0
- morphsdk/_constants.py +53 -0
- morphsdk/_errors.py +111 -0
- morphsdk/_providers/__init__.py +36 -0
- morphsdk/_providers/_filter.py +92 -0
- morphsdk/_providers/base.py +94 -0
- morphsdk/_providers/code_storage_http.py +104 -0
- morphsdk/_providers/local.py +270 -0
- morphsdk/_providers/remote.py +161 -0
- morphsdk/_version.py +1 -0
- morphsdk/adapters/__init__.py +1 -0
- morphsdk/adapters/anthropic.py +360 -0
- morphsdk/adapters/langchain.py +120 -0
- morphsdk/adapters/openai.py +500 -0
- morphsdk/py.typed +0 -0
- morphsdk/resources/__init__.py +0 -0
- morphsdk/resources/browser.py +919 -0
- morphsdk/resources/compact.py +133 -0
- morphsdk/resources/edit.py +506 -0
- morphsdk/resources/explore.py +333 -0
- morphsdk/resources/git.py +861 -0
- morphsdk/resources/github.py +1214 -0
- morphsdk/resources/grep.py +583 -0
- morphsdk/resources/mobile.py +134 -0
- morphsdk/resources/reflex.py +414 -0
- morphsdk/resources/router.py +124 -0
- morphsdk/resources/search.py +110 -0
- morphsdk/tracing/__init__.py +70 -0
- morphsdk/tracing/_otel.py +101 -0
- morphsdk/tracing/core.py +249 -0
- morphsdk/tracing/interaction.py +284 -0
- morphsdk/tracing/otel.py +75 -0
- morphsdk/tracing/reflex.py +58 -0
- morphsdk/tracing/types.py +163 -0
- morphsdk/types/__init__.py +140 -0
- morphsdk/types/browser.py +118 -0
- morphsdk/types/compact.py +41 -0
- morphsdk/types/edit.py +31 -0
- morphsdk/types/explore.py +42 -0
- morphsdk/types/git.py +25 -0
- morphsdk/types/github.py +111 -0
- morphsdk/types/grep.py +41 -0
- morphsdk/types/mobile.py +25 -0
- morphsdk/types/reflex.py +137 -0
- morphsdk/types/router.py +21 -0
- morphsdk/types/search.py +33 -0
- morphsdk-0.2.5.dist-info/METADATA +226 -0
- morphsdk-0.2.5.dist-info/RECORD +61 -0
- morphsdk-0.2.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Compact resource -- context compression with line-range awareness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from morphsdk._constants import API_BASE_URL, COMPACT_MODEL, COMPACT_TIMEOUT
|
|
8
|
+
from morphsdk.types.compact import CompactResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from morphsdk._base import AsyncBaseClient, BaseClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _build_compact_body(
|
|
15
|
+
*,
|
|
16
|
+
input: str | list[dict[str, Any]] | None,
|
|
17
|
+
messages: list[dict[str, Any]] | None,
|
|
18
|
+
query: str | None,
|
|
19
|
+
compression_ratio: float,
|
|
20
|
+
preserve_recent: int,
|
|
21
|
+
include_line_ranges: bool,
|
|
22
|
+
include_markers: bool,
|
|
23
|
+
model: str | None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Build the ``/v1/compact`` request body.
|
|
26
|
+
|
|
27
|
+
Normalizes the *messages*/*input* variants: *messages* takes priority,
|
|
28
|
+
then a string *input*, then a list *input*. Raises
|
|
29
|
+
:class:`~morphsdk._errors.ValidationError` if neither is provided.
|
|
30
|
+
"""
|
|
31
|
+
body: dict[str, Any] = {
|
|
32
|
+
"compression_ratio": compression_ratio,
|
|
33
|
+
"preserve_recent": preserve_recent,
|
|
34
|
+
"model": model or COMPACT_MODEL,
|
|
35
|
+
"include_line_ranges": include_line_ranges,
|
|
36
|
+
"include_markers": include_markers,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if query is not None:
|
|
40
|
+
body["query"] = query
|
|
41
|
+
|
|
42
|
+
if messages is not None:
|
|
43
|
+
body["messages"] = messages
|
|
44
|
+
elif isinstance(input, str):
|
|
45
|
+
body["input"] = input
|
|
46
|
+
elif isinstance(input, list):
|
|
47
|
+
body["messages"] = input
|
|
48
|
+
else:
|
|
49
|
+
from morphsdk._errors import ValidationError
|
|
50
|
+
|
|
51
|
+
raise ValidationError("Either 'input' or 'messages' must be provided")
|
|
52
|
+
|
|
53
|
+
return body
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compact(
|
|
57
|
+
client: BaseClient,
|
|
58
|
+
*,
|
|
59
|
+
input: str | list[dict[str, Any]] | None = None,
|
|
60
|
+
messages: list[dict[str, Any]] | None = None,
|
|
61
|
+
query: str | None = None,
|
|
62
|
+
compression_ratio: float = 0.5,
|
|
63
|
+
preserve_recent: int = 2,
|
|
64
|
+
include_line_ranges: bool = True,
|
|
65
|
+
include_markers: bool = True,
|
|
66
|
+
model: str | None = None,
|
|
67
|
+
timeout: float | None = None,
|
|
68
|
+
) -> CompactResult:
|
|
69
|
+
"""Compact messages or text via ``/v1/compact``.
|
|
70
|
+
|
|
71
|
+
Accepts either a plain *input* string, a list of message dicts via
|
|
72
|
+
*input*, or a separate *messages* keyword. Returns per-message
|
|
73
|
+
``compacted_line_ranges`` showing which lines were removed.
|
|
74
|
+
|
|
75
|
+
*include_line_ranges* controls whether the response includes
|
|
76
|
+
``compacted_line_ranges`` metadata (default ``True``). *include_markers*
|
|
77
|
+
controls whether ``(filtered N lines)`` text markers are inserted; when
|
|
78
|
+
``False``, removed gaps collapse to empty lines (default ``True``).
|
|
79
|
+
"""
|
|
80
|
+
body = _build_compact_body(
|
|
81
|
+
input=input,
|
|
82
|
+
messages=messages,
|
|
83
|
+
query=query,
|
|
84
|
+
compression_ratio=compression_ratio,
|
|
85
|
+
preserve_recent=preserve_recent,
|
|
86
|
+
include_line_ranges=include_line_ranges,
|
|
87
|
+
include_markers=include_markers,
|
|
88
|
+
model=model,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = client._request(
|
|
92
|
+
"POST",
|
|
93
|
+
f"{API_BASE_URL}/v1/compact",
|
|
94
|
+
json=body,
|
|
95
|
+
timeout=timeout or COMPACT_TIMEOUT,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return CompactResult.model_validate(response.json())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def acompact(
|
|
102
|
+
client: AsyncBaseClient,
|
|
103
|
+
*,
|
|
104
|
+
input: str | list[dict[str, Any]] | None = None,
|
|
105
|
+
messages: list[dict[str, Any]] | None = None,
|
|
106
|
+
query: str | None = None,
|
|
107
|
+
compression_ratio: float = 0.5,
|
|
108
|
+
preserve_recent: int = 2,
|
|
109
|
+
include_line_ranges: bool = True,
|
|
110
|
+
include_markers: bool = True,
|
|
111
|
+
model: str | None = None,
|
|
112
|
+
timeout: float | None = None,
|
|
113
|
+
) -> CompactResult:
|
|
114
|
+
"""Async counterpart of :func:`compact`. Same wire contract."""
|
|
115
|
+
body = _build_compact_body(
|
|
116
|
+
input=input,
|
|
117
|
+
messages=messages,
|
|
118
|
+
query=query,
|
|
119
|
+
compression_ratio=compression_ratio,
|
|
120
|
+
preserve_recent=preserve_recent,
|
|
121
|
+
include_line_ranges=include_line_ranges,
|
|
122
|
+
include_markers=include_markers,
|
|
123
|
+
model=model,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
response = await client._request(
|
|
127
|
+
"POST",
|
|
128
|
+
f"{API_BASE_URL}/v1/compact",
|
|
129
|
+
json=body,
|
|
130
|
+
timeout=timeout or COMPACT_TIMEOUT,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return CompactResult.model_validate(response.json())
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""Fast Apply resource -- AI-powered code editing with intelligent merge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from morphsdk._constants import API_BASE_URL, FAST_APPLY_MODEL_FAST, FAST_APPLY_MODEL_LARGE
|
|
12
|
+
from morphsdk.types.edit import ApplyEditResult, EditChanges, EditFileResult
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from morphsdk._base import AsyncBaseClient, BaseClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("morphsdk")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _generate_udiff(original: str, modified: str, filepath: str) -> str:
|
|
21
|
+
"""Generate a unified diff between *original* and *modified*."""
|
|
22
|
+
return "".join(
|
|
23
|
+
difflib.unified_diff(
|
|
24
|
+
original.splitlines(keepends=True),
|
|
25
|
+
modified.splitlines(keepends=True),
|
|
26
|
+
fromfile=filepath,
|
|
27
|
+
tofile=filepath,
|
|
28
|
+
lineterm="",
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count_changes(original: str, modified: str) -> EditChanges:
|
|
34
|
+
"""Count added / removed / modified lines from a unified diff."""
|
|
35
|
+
diff = list(
|
|
36
|
+
difflib.unified_diff(
|
|
37
|
+
original.splitlines(keepends=True),
|
|
38
|
+
modified.splitlines(keepends=True),
|
|
39
|
+
lineterm="",
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
added = 0
|
|
44
|
+
removed = 0
|
|
45
|
+
for line in diff:
|
|
46
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
47
|
+
added += 1
|
|
48
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
49
|
+
removed += 1
|
|
50
|
+
|
|
51
|
+
modified_count = min(added, removed)
|
|
52
|
+
return EditChanges(
|
|
53
|
+
lines_added=added - modified_count,
|
|
54
|
+
lines_removed=removed - modified_count,
|
|
55
|
+
lines_modified=modified_count,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_message(instruction: str, original_code: str, code_edit: str) -> str:
|
|
60
|
+
return (
|
|
61
|
+
f"<instruction>{instruction}</instruction>\n"
|
|
62
|
+
f"<code>{original_code}</code>\n"
|
|
63
|
+
f"<update>{code_edit}</update>"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_edit_result(result: EditFileResult) -> str:
|
|
68
|
+
"""Human-readable summary of an applied edit (shared by tool adapters)."""
|
|
69
|
+
c = result.changes
|
|
70
|
+
return (
|
|
71
|
+
f"Successfully applied changes to {result.path}. "
|
|
72
|
+
f"Added {c.lines_added}, removed {c.lines_removed}, modified {c.lines_modified}."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_apply_body(
|
|
77
|
+
original_code: str, code_edit: str, instruction: str
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Build the ``/v1/chat/completions`` body for a Fast Apply merge."""
|
|
80
|
+
message = _build_message(instruction, original_code, code_edit)
|
|
81
|
+
model = (
|
|
82
|
+
FAST_APPLY_MODEL_LARGE
|
|
83
|
+
if os.environ.get("MORPH_LARGE_APPLY", "true") != "false"
|
|
84
|
+
else FAST_APPLY_MODEL_FAST
|
|
85
|
+
)
|
|
86
|
+
return {"model": model, "messages": [{"role": "user", "content": message}]}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_apply_response(data: dict[str, Any]) -> tuple[str, str | None]:
|
|
90
|
+
"""Parse a Fast Apply chat-completions response into ``(merged_code, id)``."""
|
|
91
|
+
content = data["choices"][0]["message"]["content"]
|
|
92
|
+
if not content:
|
|
93
|
+
from morphsdk._errors import MorphError
|
|
94
|
+
|
|
95
|
+
raise MorphError("Morph API returned empty response")
|
|
96
|
+
|
|
97
|
+
return content, data.get("id")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_edit_path(path: str, base_dir: str | None) -> str | None:
|
|
101
|
+
"""Resolve *path* under *base_dir* (or cwd), guarding against escape.
|
|
102
|
+
|
|
103
|
+
Returns the normalized absolute path, or ``None`` if it escapes *base_dir*.
|
|
104
|
+
"""
|
|
105
|
+
base = base_dir or os.getcwd()
|
|
106
|
+
full_path = os.path.normpath(os.path.join(base, path))
|
|
107
|
+
if not full_path.startswith(os.path.normpath(base)):
|
|
108
|
+
return None
|
|
109
|
+
return full_path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _read_original(full_path: str) -> str:
|
|
113
|
+
"""Read existing file content, or an empty string for new files."""
|
|
114
|
+
try:
|
|
115
|
+
return Path(full_path).read_text(encoding="utf-8")
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _write_merged(full_path: str, merged_code: str) -> None:
|
|
121
|
+
"""Write *merged_code* to *full_path*, creating parent directories."""
|
|
122
|
+
Path(full_path).parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
Path(full_path).write_text(merged_code, encoding="utf-8")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _assemble_file_result(
|
|
127
|
+
*,
|
|
128
|
+
path: str,
|
|
129
|
+
original_code: str,
|
|
130
|
+
merged_code: str,
|
|
131
|
+
completion_id: str | None,
|
|
132
|
+
generate_udiff: bool,
|
|
133
|
+
) -> EditFileResult:
|
|
134
|
+
"""Build the public :class:`EditFileResult` from a merge."""
|
|
135
|
+
udiff = _generate_udiff(original_code, merged_code, path) if generate_udiff else None
|
|
136
|
+
return EditFileResult(
|
|
137
|
+
path=path,
|
|
138
|
+
udiff=udiff,
|
|
139
|
+
changes=_count_changes(original_code, merged_code),
|
|
140
|
+
completion_id=completion_id,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _assemble_code_result(
|
|
145
|
+
*,
|
|
146
|
+
original_code: str,
|
|
147
|
+
merged_code: str,
|
|
148
|
+
completion_id: str | None,
|
|
149
|
+
generate_udiff: bool,
|
|
150
|
+
) -> ApplyEditResult:
|
|
151
|
+
"""Build the public :class:`ApplyEditResult` from a merge."""
|
|
152
|
+
udiff = _generate_udiff(original_code, merged_code, "file") if generate_udiff else None
|
|
153
|
+
return ApplyEditResult(
|
|
154
|
+
merged_code=merged_code,
|
|
155
|
+
udiff=udiff,
|
|
156
|
+
changes=_count_changes(original_code, merged_code),
|
|
157
|
+
completion_id=completion_id,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class EditResource:
|
|
162
|
+
"""AI-powered code editing via Morph Fast Apply."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, client: BaseClient) -> None:
|
|
165
|
+
self._client = client
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# file() -- read from disk, merge, write back
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def file(
|
|
172
|
+
self,
|
|
173
|
+
*,
|
|
174
|
+
path: str,
|
|
175
|
+
instruction: str,
|
|
176
|
+
code_edit: str,
|
|
177
|
+
base_dir: str | None = None,
|
|
178
|
+
generate_udiff: bool = True,
|
|
179
|
+
auto_write: bool = True,
|
|
180
|
+
timeout: float | None = None,
|
|
181
|
+
) -> EditFileResult:
|
|
182
|
+
"""Edit a file on disk using Morph Fast Apply.
|
|
183
|
+
|
|
184
|
+
1. Resolves *path* relative to *base_dir* (or cwd).
|
|
185
|
+
2. Reads current content (empty string for new files).
|
|
186
|
+
3. Calls the Morph merge API.
|
|
187
|
+
4. Optionally writes the merged result and generates a udiff.
|
|
188
|
+
"""
|
|
189
|
+
full_path = _resolve_edit_path(path, base_dir)
|
|
190
|
+
if full_path is None:
|
|
191
|
+
return EditFileResult(
|
|
192
|
+
path=path,
|
|
193
|
+
changes=EditChanges(lines_added=0, lines_removed=0, lines_modified=0),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
original_code = _read_original(full_path)
|
|
197
|
+
|
|
198
|
+
merged_code, completion_id = self._call_api(
|
|
199
|
+
original_code, code_edit, instruction, timeout=timeout
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if auto_write:
|
|
203
|
+
_write_merged(full_path, merged_code)
|
|
204
|
+
|
|
205
|
+
return _assemble_file_result(
|
|
206
|
+
path=path,
|
|
207
|
+
original_code=original_code,
|
|
208
|
+
merged_code=merged_code,
|
|
209
|
+
completion_id=completion_id,
|
|
210
|
+
generate_udiff=generate_udiff,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# code() -- pure code-in / code-out, no file I/O
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def code(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
original_code: str,
|
|
221
|
+
code_edit: str,
|
|
222
|
+
instruction: str,
|
|
223
|
+
generate_udiff: bool = True,
|
|
224
|
+
timeout: float | None = None,
|
|
225
|
+
) -> ApplyEditResult:
|
|
226
|
+
"""Apply an edit to code directly without file I/O."""
|
|
227
|
+
merged_code, completion_id = self._call_api(
|
|
228
|
+
original_code, code_edit, instruction, timeout=timeout
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return _assemble_code_result(
|
|
232
|
+
original_code=original_code,
|
|
233
|
+
merged_code=merged_code,
|
|
234
|
+
completion_id=completion_id,
|
|
235
|
+
generate_udiff=generate_udiff,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
# Internal
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------
|
|
243
|
+
# Framework adapters
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def as_openai_tool(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
base_dir: str | None = None,
|
|
250
|
+
description: str | None = None,
|
|
251
|
+
) -> Any:
|
|
252
|
+
"""Create an OpenAI ChatCompletionTool for edit_file."""
|
|
253
|
+
from morphsdk.adapters.openai import (
|
|
254
|
+
EDIT_FILE_SYSTEM_PROMPT,
|
|
255
|
+
OpenAITool,
|
|
256
|
+
edit_file_tool_def,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
effective_base_dir = base_dir
|
|
260
|
+
|
|
261
|
+
def executor(
|
|
262
|
+
target_filepath: str,
|
|
263
|
+
instruction: str,
|
|
264
|
+
code_edit: str,
|
|
265
|
+
) -> EditFileResult:
|
|
266
|
+
return self.file(
|
|
267
|
+
path=target_filepath,
|
|
268
|
+
instruction=instruction,
|
|
269
|
+
code_edit=code_edit,
|
|
270
|
+
base_dir=effective_base_dir,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return OpenAITool(
|
|
274
|
+
definition=edit_file_tool_def(description=description),
|
|
275
|
+
executor=executor,
|
|
276
|
+
formatter=_format_edit_result,
|
|
277
|
+
system_prompt=EDIT_FILE_SYSTEM_PROMPT,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def as_anthropic_tool(
|
|
281
|
+
self,
|
|
282
|
+
*,
|
|
283
|
+
base_dir: str | None = None,
|
|
284
|
+
description: str | None = None,
|
|
285
|
+
) -> Any:
|
|
286
|
+
"""Create an Anthropic Tool for edit_file."""
|
|
287
|
+
from morphsdk.adapters.anthropic import AnthropicTool, edit_file_tool_def
|
|
288
|
+
from morphsdk.adapters.openai import EDIT_FILE_SYSTEM_PROMPT
|
|
289
|
+
|
|
290
|
+
effective_base_dir = base_dir
|
|
291
|
+
|
|
292
|
+
def executor(
|
|
293
|
+
target_filepath: str,
|
|
294
|
+
instruction: str,
|
|
295
|
+
code_edit: str,
|
|
296
|
+
) -> EditFileResult:
|
|
297
|
+
return self.file(
|
|
298
|
+
path=target_filepath,
|
|
299
|
+
instruction=instruction,
|
|
300
|
+
code_edit=code_edit,
|
|
301
|
+
base_dir=effective_base_dir,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return AnthropicTool(
|
|
305
|
+
definition=edit_file_tool_def(description=description),
|
|
306
|
+
executor=executor,
|
|
307
|
+
formatter=_format_edit_result,
|
|
308
|
+
system_prompt=EDIT_FILE_SYSTEM_PROMPT,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Internal
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def _call_api(
|
|
316
|
+
self,
|
|
317
|
+
original_code: str,
|
|
318
|
+
code_edit: str,
|
|
319
|
+
instruction: str,
|
|
320
|
+
*,
|
|
321
|
+
timeout: float | None = None,
|
|
322
|
+
) -> tuple[str, str | None]:
|
|
323
|
+
"""Call the Morph Fast Apply chat completions endpoint.
|
|
324
|
+
|
|
325
|
+
Returns ``(merged_code, completion_id)``.
|
|
326
|
+
"""
|
|
327
|
+
response = self._client._request(
|
|
328
|
+
"POST",
|
|
329
|
+
f"{API_BASE_URL}/v1/chat/completions",
|
|
330
|
+
json=_build_apply_body(original_code, code_edit, instruction),
|
|
331
|
+
timeout=timeout,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return _parse_apply_response(response.json())
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class AsyncEditResource:
|
|
338
|
+
"""Async AI-powered code editing via Morph Fast Apply."""
|
|
339
|
+
|
|
340
|
+
def __init__(self, client: AsyncBaseClient) -> None:
|
|
341
|
+
self._client = client
|
|
342
|
+
|
|
343
|
+
# ------------------------------------------------------------------
|
|
344
|
+
# file() -- read from disk, merge, write back
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
async def file(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
path: str,
|
|
351
|
+
instruction: str,
|
|
352
|
+
code_edit: str,
|
|
353
|
+
base_dir: str | None = None,
|
|
354
|
+
generate_udiff: bool = True,
|
|
355
|
+
auto_write: bool = True,
|
|
356
|
+
timeout: float | None = None,
|
|
357
|
+
) -> EditFileResult:
|
|
358
|
+
"""Edit a file on disk using Morph Fast Apply.
|
|
359
|
+
|
|
360
|
+
1. Resolves *path* relative to *base_dir* (or cwd).
|
|
361
|
+
2. Reads current content (empty string for new files).
|
|
362
|
+
3. Calls the Morph merge API.
|
|
363
|
+
4. Optionally writes the merged result and generates a udiff.
|
|
364
|
+
"""
|
|
365
|
+
full_path = _resolve_edit_path(path, base_dir)
|
|
366
|
+
if full_path is None:
|
|
367
|
+
return EditFileResult(
|
|
368
|
+
path=path,
|
|
369
|
+
changes=EditChanges(lines_added=0, lines_removed=0, lines_modified=0),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
original_code = _read_original(full_path)
|
|
373
|
+
|
|
374
|
+
merged_code, completion_id = await self._call_api(
|
|
375
|
+
original_code, code_edit, instruction, timeout=timeout
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if auto_write:
|
|
379
|
+
_write_merged(full_path, merged_code)
|
|
380
|
+
|
|
381
|
+
return _assemble_file_result(
|
|
382
|
+
path=path,
|
|
383
|
+
original_code=original_code,
|
|
384
|
+
merged_code=merged_code,
|
|
385
|
+
completion_id=completion_id,
|
|
386
|
+
generate_udiff=generate_udiff,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# ------------------------------------------------------------------
|
|
390
|
+
# code() -- pure code-in / code-out, no file I/O
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
async def code(
|
|
394
|
+
self,
|
|
395
|
+
*,
|
|
396
|
+
original_code: str,
|
|
397
|
+
code_edit: str,
|
|
398
|
+
instruction: str,
|
|
399
|
+
generate_udiff: bool = True,
|
|
400
|
+
timeout: float | None = None,
|
|
401
|
+
) -> ApplyEditResult:
|
|
402
|
+
"""Apply an edit to code directly without file I/O."""
|
|
403
|
+
merged_code, completion_id = await self._call_api(
|
|
404
|
+
original_code, code_edit, instruction, timeout=timeout
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return _assemble_code_result(
|
|
408
|
+
original_code=original_code,
|
|
409
|
+
merged_code=merged_code,
|
|
410
|
+
completion_id=completion_id,
|
|
411
|
+
generate_udiff=generate_udiff,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# ------------------------------------------------------------------
|
|
415
|
+
# Framework adapters
|
|
416
|
+
# ------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def as_openai_tool(
|
|
419
|
+
self,
|
|
420
|
+
*,
|
|
421
|
+
base_dir: str | None = None,
|
|
422
|
+
description: str | None = None,
|
|
423
|
+
) -> Any:
|
|
424
|
+
"""Create an OpenAI ChatCompletionTool for edit_file with an async executor."""
|
|
425
|
+
from morphsdk.adapters.openai import (
|
|
426
|
+
EDIT_FILE_SYSTEM_PROMPT,
|
|
427
|
+
OpenAITool,
|
|
428
|
+
edit_file_tool_def,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
effective_base_dir = base_dir
|
|
432
|
+
|
|
433
|
+
async def executor(
|
|
434
|
+
target_filepath: str,
|
|
435
|
+
instruction: str,
|
|
436
|
+
code_edit: str,
|
|
437
|
+
) -> EditFileResult:
|
|
438
|
+
return await self.file(
|
|
439
|
+
path=target_filepath,
|
|
440
|
+
instruction=instruction,
|
|
441
|
+
code_edit=code_edit,
|
|
442
|
+
base_dir=effective_base_dir,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return OpenAITool(
|
|
446
|
+
definition=edit_file_tool_def(description=description),
|
|
447
|
+
executor=executor,
|
|
448
|
+
formatter=_format_edit_result,
|
|
449
|
+
system_prompt=EDIT_FILE_SYSTEM_PROMPT,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def as_anthropic_tool(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
base_dir: str | None = None,
|
|
456
|
+
description: str | None = None,
|
|
457
|
+
) -> Any:
|
|
458
|
+
"""Create an Anthropic Tool for edit_file with an async executor."""
|
|
459
|
+
from morphsdk.adapters.anthropic import AnthropicTool, edit_file_tool_def
|
|
460
|
+
from morphsdk.adapters.openai import EDIT_FILE_SYSTEM_PROMPT
|
|
461
|
+
|
|
462
|
+
effective_base_dir = base_dir
|
|
463
|
+
|
|
464
|
+
async def executor(
|
|
465
|
+
target_filepath: str,
|
|
466
|
+
instruction: str,
|
|
467
|
+
code_edit: str,
|
|
468
|
+
) -> EditFileResult:
|
|
469
|
+
return await self.file(
|
|
470
|
+
path=target_filepath,
|
|
471
|
+
instruction=instruction,
|
|
472
|
+
code_edit=code_edit,
|
|
473
|
+
base_dir=effective_base_dir,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return AnthropicTool(
|
|
477
|
+
definition=edit_file_tool_def(description=description),
|
|
478
|
+
executor=executor,
|
|
479
|
+
formatter=_format_edit_result,
|
|
480
|
+
system_prompt=EDIT_FILE_SYSTEM_PROMPT,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# ------------------------------------------------------------------
|
|
484
|
+
# Internal
|
|
485
|
+
# ------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
async def _call_api(
|
|
488
|
+
self,
|
|
489
|
+
original_code: str,
|
|
490
|
+
code_edit: str,
|
|
491
|
+
instruction: str,
|
|
492
|
+
*,
|
|
493
|
+
timeout: float | None = None,
|
|
494
|
+
) -> tuple[str, str | None]:
|
|
495
|
+
"""Call the Morph Fast Apply chat completions endpoint.
|
|
496
|
+
|
|
497
|
+
Returns ``(merged_code, completion_id)``.
|
|
498
|
+
"""
|
|
499
|
+
response = await self._client._request(
|
|
500
|
+
"POST",
|
|
501
|
+
f"{API_BASE_URL}/v1/chat/completions",
|
|
502
|
+
json=_build_apply_body(original_code, code_edit, instruction),
|
|
503
|
+
timeout=timeout,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return _parse_apply_response(response.json())
|