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.
Files changed (61) hide show
  1. morphsdk/__init__.py +54 -0
  2. morphsdk/_agent/__init__.py +64 -0
  3. morphsdk/_agent/config.py +52 -0
  4. morphsdk/_agent/explore.py +276 -0
  5. morphsdk/_agent/github.py +57 -0
  6. morphsdk/_agent/helpers.py +133 -0
  7. morphsdk/_agent/parser.py +163 -0
  8. morphsdk/_agent/runner.py +524 -0
  9. morphsdk/_agent/tools.py +171 -0
  10. morphsdk/_agent/types.py +126 -0
  11. morphsdk/_base.py +309 -0
  12. morphsdk/_client.py +245 -0
  13. morphsdk/_config.py +37 -0
  14. morphsdk/_constants.py +53 -0
  15. morphsdk/_errors.py +111 -0
  16. morphsdk/_providers/__init__.py +36 -0
  17. morphsdk/_providers/_filter.py +92 -0
  18. morphsdk/_providers/base.py +94 -0
  19. morphsdk/_providers/code_storage_http.py +104 -0
  20. morphsdk/_providers/local.py +270 -0
  21. morphsdk/_providers/remote.py +161 -0
  22. morphsdk/_version.py +1 -0
  23. morphsdk/adapters/__init__.py +1 -0
  24. morphsdk/adapters/anthropic.py +360 -0
  25. morphsdk/adapters/langchain.py +120 -0
  26. morphsdk/adapters/openai.py +500 -0
  27. morphsdk/py.typed +0 -0
  28. morphsdk/resources/__init__.py +0 -0
  29. morphsdk/resources/browser.py +919 -0
  30. morphsdk/resources/compact.py +133 -0
  31. morphsdk/resources/edit.py +506 -0
  32. morphsdk/resources/explore.py +333 -0
  33. morphsdk/resources/git.py +861 -0
  34. morphsdk/resources/github.py +1214 -0
  35. morphsdk/resources/grep.py +583 -0
  36. morphsdk/resources/mobile.py +134 -0
  37. morphsdk/resources/reflex.py +414 -0
  38. morphsdk/resources/router.py +124 -0
  39. morphsdk/resources/search.py +110 -0
  40. morphsdk/tracing/__init__.py +70 -0
  41. morphsdk/tracing/_otel.py +101 -0
  42. morphsdk/tracing/core.py +249 -0
  43. morphsdk/tracing/interaction.py +284 -0
  44. morphsdk/tracing/otel.py +75 -0
  45. morphsdk/tracing/reflex.py +58 -0
  46. morphsdk/tracing/types.py +163 -0
  47. morphsdk/types/__init__.py +140 -0
  48. morphsdk/types/browser.py +118 -0
  49. morphsdk/types/compact.py +41 -0
  50. morphsdk/types/edit.py +31 -0
  51. morphsdk/types/explore.py +42 -0
  52. morphsdk/types/git.py +25 -0
  53. morphsdk/types/github.py +111 -0
  54. morphsdk/types/grep.py +41 -0
  55. morphsdk/types/mobile.py +25 -0
  56. morphsdk/types/reflex.py +137 -0
  57. morphsdk/types/router.py +21 -0
  58. morphsdk/types/search.py +33 -0
  59. morphsdk-0.2.5.dist-info/METADATA +226 -0
  60. morphsdk-0.2.5.dist-info/RECORD +61 -0
  61. morphsdk-0.2.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,360 @@
1
+ """Anthropic framework adapter -- generates Anthropic Tool-compatible dicts.
2
+
3
+ Each tool definition follows the Anthropic tool schema::
4
+
5
+ {
6
+ "name": "...",
7
+ "description": "...",
8
+ "input_schema": { JSON Schema }
9
+ }
10
+
11
+ The :class:`AnthropicTool` wrapper pairs the definition with an ``execute()``
12
+ method so callers can dispatch tool calls in a single object.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from collections.abc import ItemsView, Iterator, KeysView, ValuesView
19
+ from typing import Any, Callable
20
+
21
+ from .openai import (
22
+ BROWSER_TOOL_DESCRIPTION,
23
+ CODEBASE_SEARCH_DESCRIPTION,
24
+ EDIT_FILE_DESCRIPTION,
25
+ GITHUB_READ_FILE_DESCRIPTION,
26
+ GITHUB_SEARCH_DESCRIPTION,
27
+ MOBILE_TOOL_DESCRIPTION,
28
+ WARP_GREP_DESCRIPTION,
29
+ )
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Tool definition factories
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def edit_file_tool_def(*, description: str | None = None) -> dict[str, Any]:
36
+ """Generate an Anthropic Tool dict for ``edit_file``."""
37
+ return {
38
+ "name": "edit_file",
39
+ "description": description or EDIT_FILE_DESCRIPTION,
40
+ "input_schema": {
41
+ "type": "object",
42
+ "properties": {
43
+ "target_filepath": {
44
+ "type": "string",
45
+ "description": "The path of the target file to modify",
46
+ },
47
+ "instruction": {
48
+ "type": "string",
49
+ "description": (
50
+ "A single sentence written in the first person describing "
51
+ "what the agent is changing. Used to help disambiguate "
52
+ "uncertainty in the edit."
53
+ ),
54
+ },
55
+ "code_edit": {
56
+ "type": "string",
57
+ "description": (
58
+ "Specify ONLY the precise lines of code that you wish to "
59
+ "edit. Use `// ... existing code ...` for unchanged sections."
60
+ ),
61
+ },
62
+ },
63
+ "required": ["target_filepath", "instruction", "code_edit"],
64
+ },
65
+ }
66
+
67
+
68
+ def codebase_search_tool_def(*, description: str | None = None) -> dict[str, Any]:
69
+ """Generate an Anthropic Tool dict for ``codebase_search``."""
70
+ return {
71
+ "name": "codebase_search",
72
+ "description": description or CODEBASE_SEARCH_DESCRIPTION,
73
+ "input_schema": {
74
+ "type": "object",
75
+ "properties": {
76
+ "explanation": {
77
+ "type": "string",
78
+ "description": (
79
+ "One sentence explanation as to why this tool is being used, "
80
+ "and how it contributes to the goal."
81
+ ),
82
+ },
83
+ "query": {
84
+ "type": "string",
85
+ "description": (
86
+ "A complete question about what you want to understand. "
87
+ "Ask as if talking to a colleague: \"How does X work?\", "
88
+ "\"What happens when Y?\", \"Where is Z handled?\""
89
+ ),
90
+ },
91
+ "target_directories": {
92
+ "type": "array",
93
+ "items": {"type": "string"},
94
+ "description": (
95
+ "Prefix directory paths to limit search scope "
96
+ "(single directory only, no glob patterns). "
97
+ "Use [] to search entire repo."
98
+ ),
99
+ },
100
+ "limit": {
101
+ "type": "number",
102
+ "description": "Maximum results to return (default: 10)",
103
+ },
104
+ },
105
+ "required": ["query", "target_directories", "explanation"],
106
+ },
107
+ }
108
+
109
+
110
+ def grep_tool_def(
111
+ *,
112
+ name: str = "codebase_search",
113
+ description: str | None = None,
114
+ ) -> dict[str, Any]:
115
+ """Generate an Anthropic Tool dict for the WarpGrep agent."""
116
+ return {
117
+ "name": name,
118
+ "description": description or WARP_GREP_DESCRIPTION,
119
+ "input_schema": {
120
+ "type": "object",
121
+ "properties": {
122
+ "search_term": {
123
+ "type": "string",
124
+ "description": (
125
+ "Search problem statement that this subagent is "
126
+ "supposed to research for"
127
+ ),
128
+ },
129
+ },
130
+ "required": ["search_term"],
131
+ },
132
+ }
133
+
134
+
135
+ def github_search_tool_def(*, description: str | None = None) -> dict[str, Any]:
136
+ """Generate an Anthropic Tool dict for ``github_codebase_search``."""
137
+ return {
138
+ "name": "github_codebase_search",
139
+ "description": description or GITHUB_SEARCH_DESCRIPTION,
140
+ "input_schema": {
141
+ "type": "object",
142
+ "properties": {
143
+ "search_term": {
144
+ "type": "string",
145
+ "description": (
146
+ "A targeted natural-language query describing what to find "
147
+ "(e.g. \"How does auth work for SSO users?\"). "
148
+ "Not a keyword or symbol name."
149
+ ),
150
+ },
151
+ "github_url": {
152
+ "type": "string",
153
+ "description": (
154
+ "GitHub repository URL to search "
155
+ "(e.g. \"https://github.com/vercel/next.js\"). "
156
+ "You must provide either github_url or owner_repo."
157
+ ),
158
+ },
159
+ "owner_repo": {
160
+ "type": "string",
161
+ "description": (
162
+ "Repository owner/repo shorthand (e.g. \"vercel/next.js\"). "
163
+ "You must provide either github_url or owner_repo."
164
+ ),
165
+ },
166
+ "branch": {
167
+ "type": "string",
168
+ "description": "Branch to search (defaults to the repository default branch)",
169
+ },
170
+ },
171
+ "required": ["search_term"],
172
+ },
173
+ }
174
+
175
+
176
+ def github_read_file_tool_def(*, description: str | None = None) -> dict[str, Any]:
177
+ """Generate an Anthropic Tool dict for ``readfile_github_search``."""
178
+ return {
179
+ "name": "readfile_github_search",
180
+ "description": description or GITHUB_READ_FILE_DESCRIPTION,
181
+ "input_schema": {
182
+ "type": "object",
183
+ "properties": {
184
+ "github": {
185
+ "type": "string",
186
+ "description": "owner/repo shorthand (e.g., \"vercel/next.js\")",
187
+ },
188
+ "path": {
189
+ "type": "string",
190
+ "description": (
191
+ "File path within the repository (e.g., \"src/server/index.ts\")"
192
+ ),
193
+ },
194
+ "startLine": {
195
+ "type": "number",
196
+ "description": "Start line number (1-based). Omit to start from beginning.",
197
+ },
198
+ "endLine": {
199
+ "type": "number",
200
+ "description": "End line number (1-based, inclusive). Omit to read to the end.",
201
+ },
202
+ "branch": {
203
+ "type": "string",
204
+ "description": (
205
+ "Branch to read from (defaults to the repository default branch)"
206
+ ),
207
+ },
208
+ },
209
+ "required": ["github", "path"],
210
+ },
211
+ }
212
+
213
+
214
+ def browser_tool_def(*, description: str | None = None) -> dict[str, Any]:
215
+ """Generate an Anthropic Tool dict for ``browser_task``."""
216
+ return {
217
+ "name": "browser_task",
218
+ "description": description or BROWSER_TOOL_DESCRIPTION,
219
+ "input_schema": {
220
+ "type": "object",
221
+ "properties": {
222
+ "task": {
223
+ "type": "string",
224
+ "description": (
225
+ "Natural language description of what to do "
226
+ "(e.g., \"Test checkout flow for buying a pineapple\")"
227
+ ),
228
+ },
229
+ "url": {
230
+ "type": "string",
231
+ "description": (
232
+ "Starting URL (e.g., https://3000-xyz.e2b.dev). "
233
+ "Required if navigating to a specific page."
234
+ ),
235
+ },
236
+ "maxSteps": {
237
+ "type": "number",
238
+ "description": (
239
+ "Maximum number of browser actions to take (1-50). "
240
+ "Default: 10. Use 15-30 for complex flows."
241
+ ),
242
+ },
243
+ "region": {
244
+ "type": "string",
245
+ "enum": ["sfo", "lon"],
246
+ "description": (
247
+ "Browserless region: sfo (US West Coast) or lon (Europe). "
248
+ "Default: sfo."
249
+ ),
250
+ },
251
+ },
252
+ "required": ["task"],
253
+ },
254
+ }
255
+
256
+
257
+ def mobile_tool_def(*, description: str | None = None) -> dict[str, Any]:
258
+ """Generate an Anthropic Tool dict for ``mobile_task``."""
259
+ return {
260
+ "name": "mobile_task",
261
+ "description": description or MOBILE_TOOL_DESCRIPTION,
262
+ "input_schema": {
263
+ "type": "object",
264
+ "properties": {
265
+ "task": {
266
+ "type": "string",
267
+ "description": "Natural language description of what to do in the mobile app",
268
+ },
269
+ "app": {
270
+ "type": "string",
271
+ "description": "Application identifier (bundle ID or BrowserStack bs:// URL)",
272
+ },
273
+ "platform": {
274
+ "type": "string",
275
+ "enum": ["ios", "android"],
276
+ "description": "Target platform (default: ios)",
277
+ },
278
+ "max_steps": {
279
+ "type": "number",
280
+ "description": "Maximum agent steps (default: 50)",
281
+ },
282
+ },
283
+ "required": ["task", "app"],
284
+ },
285
+ }
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # AnthropicTool -- pairs a definition with an executor
290
+ # ---------------------------------------------------------------------------
291
+
292
+ class AnthropicTool:
293
+ """A tool definition paired with an executor for Anthropic messages.
294
+
295
+ Behaves like a dict (can be passed directly to ``tools=[tool]``) while
296
+ also exposing ``execute()`` and ``format_result()`` methods.
297
+
298
+ Example::
299
+
300
+ tool = morph.edit.as_anthropic_tool(base_dir="./src")
301
+
302
+ # Pass to Anthropic
303
+ response = client.messages.create(
304
+ model="claude-sonnet-4-5-20250929",
305
+ tools=[tool],
306
+ messages=[...],
307
+ )
308
+
309
+ # Execute
310
+ result = tool.execute(tool_use_block.input)
311
+ formatted = tool.format_result(result)
312
+ """
313
+
314
+ def __init__(
315
+ self,
316
+ definition: dict[str, Any],
317
+ executor: Callable[..., Any],
318
+ formatter: Callable[..., str],
319
+ system_prompt: str = "",
320
+ ) -> None:
321
+ self._definition = definition
322
+ self._executor = executor
323
+ self._formatter = formatter
324
+ self.system_prompt = system_prompt
325
+
326
+ # -- dict protocol so Anthropic SDK accepts this as a tool ---
327
+ def __getitem__(self, key: str) -> Any:
328
+ return self._definition[key]
329
+
330
+ def __contains__(self, key: str) -> bool:
331
+ return key in self._definition
332
+
333
+ def __iter__(self) -> Iterator[str]:
334
+ return iter(self._definition)
335
+
336
+ def keys(self) -> KeysView[str]:
337
+ return self._definition.keys()
338
+
339
+ def items(self) -> ItemsView[str, Any]:
340
+ return self._definition.items()
341
+
342
+ def values(self) -> ValuesView[Any]:
343
+ return self._definition.values()
344
+
345
+ def to_dict(self) -> dict[str, Any]:
346
+ """Return the raw tool definition dict."""
347
+ return self._definition
348
+
349
+ # -- execution ---
350
+ def execute(self, input: dict[str, Any] | str) -> Any:
351
+ """Execute the tool with the given input.
352
+
353
+ *input* may be a dict (from ``tool_use.input``) or a JSON string.
354
+ """
355
+ args: dict[str, Any] = json.loads(input) if isinstance(input, str) else input
356
+ return self._executor(**args)
357
+
358
+ def format_result(self, result: Any) -> str:
359
+ """Format a result for passing back as a tool_result content block."""
360
+ return self._formatter(result)
@@ -0,0 +1,120 @@
1
+ """LangChain framework adapter -- generates LangChain-compatible tool instances.
2
+
3
+ Requires ``langchain-core`` as an optional dependency. All imports are
4
+ deferred so the adapter module can be imported even when LangChain is not
5
+ installed; a clear error is raised at tool-creation time if the dependency
6
+ is missing.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any, Callable, Optional, cast
12
+
13
+ if TYPE_CHECKING:
14
+ from pydantic import BaseModel
15
+
16
+
17
+ def _ensure_langchain() -> None:
18
+ """Raise a helpful error if langchain-core is not installed."""
19
+ try:
20
+ import langchain_core # noqa: F401
21
+ except ImportError:
22
+ raise ImportError(
23
+ "LangChain adapter requires 'langchain-core'. "
24
+ "Install it with: pip install langchain-core"
25
+ ) from None
26
+
27
+
28
+ def create_tool(
29
+ *,
30
+ name: str,
31
+ description: str,
32
+ args_schema: dict[str, Any],
33
+ executor: Callable[..., Any],
34
+ formatter: Callable[..., str] | None = None,
35
+ ) -> Any:
36
+ """Create a LangChain ``StructuredTool`` from a Morph tool specification.
37
+
38
+ Parameters
39
+ ----------
40
+ name:
41
+ Tool name (e.g. ``edit_file``).
42
+ description:
43
+ Tool description shown to the LLM.
44
+ args_schema:
45
+ JSON Schema dict describing the tool parameters.
46
+ executor:
47
+ Callable that executes the tool. Receives kwargs matching the schema.
48
+ formatter:
49
+ Optional callable to format the result as a string.
50
+
51
+ Returns
52
+ -------
53
+ A ``langchain_core.tools.StructuredTool`` instance.
54
+ """
55
+ _ensure_langchain()
56
+
57
+ from langchain_core.tools import StructuredTool
58
+
59
+ def _run(**kwargs: Any) -> str:
60
+ result = executor(**kwargs)
61
+ if formatter is not None:
62
+ return formatter(result)
63
+ return str(result)
64
+
65
+ # Build a Pydantic model from the JSON schema for LangChain's args_schema
66
+ input_model = _json_schema_to_pydantic(name, args_schema)
67
+
68
+ return StructuredTool(
69
+ name=name,
70
+ description=description,
71
+ func=_run,
72
+ args_schema=input_model,
73
+ )
74
+
75
+
76
+ def _json_schema_to_pydantic(tool_name: str, schema: dict[str, Any]) -> type[BaseModel]:
77
+ """Convert a flat JSON Schema ``properties`` dict to a Pydantic model.
78
+
79
+ This handles the common case of top-level string/number/array properties.
80
+ Nested objects are left as ``dict``.
81
+ """
82
+ _ensure_langchain()
83
+
84
+ from pydantic import Field, create_model
85
+
86
+ properties = schema.get("properties", {})
87
+ required = set(schema.get("required", []))
88
+ field_definitions: dict[str, Any] = {}
89
+
90
+ _TYPE_MAP = {
91
+ "string": str,
92
+ "number": float,
93
+ "integer": int,
94
+ "boolean": bool,
95
+ "object": dict,
96
+ }
97
+
98
+ for prop_name, prop_schema in properties.items():
99
+ prop_type = prop_schema.get("type", "string")
100
+ description = prop_schema.get("description", "")
101
+
102
+ python_type: type[Any]
103
+ if prop_type == "array":
104
+ item_type = _TYPE_MAP.get(
105
+ prop_schema.get("items", {}).get("type", "string"), str
106
+ )
107
+ python_type = list[item_type] # type: ignore[valid-type]
108
+ else:
109
+ python_type = _TYPE_MAP.get(prop_type, str)
110
+
111
+ if prop_name in required:
112
+ field_definitions[prop_name] = (python_type, Field(description=description))
113
+ else:
114
+ field_definitions[prop_name] = (
115
+ Optional[python_type],
116
+ Field(default=None, description=description),
117
+ )
118
+
119
+ model_name = f"{tool_name.title().replace('_', '')}Input"
120
+ return cast("type[BaseModel]", create_model(model_name, **field_definitions))