Metadata-Version: 2.4
Name: morphsdk
Version: 0.2.6
Summary: Morph SDK - AI-powered code editing, search, browser automation, and more
Project-URL: Homepage, https://morphllm.com
Project-URL: Documentation, https://docs.morphllm.com
Author-email: Morph <support@morphllm.com>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: eval-type-backport>=0.2; python_version < '3.10'
Requires-Dist: gitpython>=3.1
Requires-Dist: httpx>=0.25
Requires-Dist: pydantic>=2.0
Provides-Extra: all
Requires-Dist: anthropic>=0.30; extra == 'all'
Requires-Dist: langchain-core>=0.2; extra == 'all'
Requires-Dist: openai>=1.0; extra == 'all'
Requires-Dist: opentelemetry-api>=1.41; extra == 'all'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.41; extra == 'all'
Requires-Dist: traceloop-sdk>=0.36; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.3; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == 'openai'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.41; extra == 'otel'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.41; extra == 'otel'
Requires-Dist: traceloop-sdk>=0.36; extra == 'otel'
Description-Content-Type: text/markdown

# Morph Python SDK

One typed client for Morph's specialized agent models: merge code edits with Fast Apply at ~10,500 tok/s, search a repo with WarpGrep, compress context with the compactor, route between models, and run browser tasks. Synchronous and async, Python 3.9+.

```bash
pip install morphsdk
```

## Quickstart

```python
from morphsdk import Morph

morph = Morph(api_key="sk-...")  # or set MORPH_API_KEY

result = morph.edit.file(
    path="src/app.py",
    instruction="Add a retry decorator to fetch_user",
    code_edit="# ... existing code ...\n@retry(max_attempts=3)\ndef fetch_user(user_id):",
)
print(result.changes)  # lines added / removed / modified
```

The API key is read from `MORPH_API_KEY` if you do not pass `api_key=`. Get one at [morphllm.com](https://morphllm.com).

## Fast Apply: merge edits onto a file

`edit.file` reads the file, sends the original plus your edit snippet to the merge API, and writes the result back. Use `auto_write=False` to get the merged text without touching disk.

```python
result = morph.edit.file(
    path="src/app.py",
    instruction="Rename the handler to on_request",
    code_edit="# ... existing code ...\ndef on_request(req):\n    # ... existing code ...",
)

# Code-in, code-out, no file I/O:
merged = morph.edit.code(
    instruction="Add type hints",
    original_code="def add(a, b):\n    return a + b",
    code_edit="def add(a: int, b: int) -> int:\n    # ... existing code ...",
)
```

## WarpGrep: search a codebase

`grep.search` runs the WarpGrep agent loop over a local repo. Pass `stream=True` to iterate per-turn steps before the final result. `search_github` does the same against a public repo.

```python
result = morph.grep.search(
    query="where is the auth middleware registered?",
    repo_root="/path/to/repo",
)

for step in morph.grep.search(query="rate limiter", repo_root="/path/to/repo", stream=True):
    print(step)

result = morph.grep.search_github(
    query="how does the router pick a model?",
    github="morphllm/landing",
)
```

## Explore: multi-turn codebase Q&A

`explore.run` answers a natural-language question about a repository and returns a summary plus the contexts it found. `thoroughness` is `quick`, `medium`, or `thorough`.

```python
result = morph.explore.run(
    query="how does the WarpGrep agent loop call the model each turn?",
    repo_root="/path/to/repo",
    thoroughness="quick",
)
print(result.summary)
for ctx in result.contexts:
    print(ctx)
```

## Compact: compress context

```python
result = morph.compact(
    input="...long transcript or code...",
    query="what matters for the next step",
    compression_ratio=0.5,
)
print(result)
```

## Router: pick a model for a prompt

```python
choice = morph.router.select_model(input="Explain quicksort")
print(choice)
```

## More resources

The same client exposes `morph.search` (semantic codebase search by `repo_id`), `morph.browser` (`browser.run(task=..., url=...)`), `morph.git`, `morph.github`, `morph.mobile`, and `morph.reflex` (per-turn classifiers, OpenAI-fine-tuning-compatible). See the [docs](https://docs.morphllm.com) for the full surface.

## Async

`AsyncMorph` mirrors `Morph` with every method as `async def`; streaming methods return an `AsyncIterator`.

```python
import asyncio
from morphsdk import AsyncMorph

async def main():
    async with AsyncMorph(api_key="sk-...") as morph:
        result = await morph.edit.file(
            path="src/app.py",
            instruction="Fix the off-by-one",
            code_edit="# ... existing code ...",
        )
        print(result.changes)

asyncio.run(main())
```

## Framework adapters

Install the extra for your agent framework to get ready-made tool definitions.

```bash
pip install "morphsdk[openai]"      # OpenAI tool schemas
pip install "morphsdk[anthropic]"   # Anthropic tool schemas
pip install "morphsdk[langchain]"   # LangChain tools
pip install "morphsdk[otel]"        # OpenTelemetry tracing
pip install "morphsdk[all]"         # everything above
```

```python
from morphsdk.adapters.openai import edit_file_tool_def

tools = [edit_file_tool_def()]
```

## Errors

Every failure raises a subclass of `MorphError`, so you can catch one type or branch on specifics.

```python
from morphsdk import (
    MorphError,
    AuthenticationError,
    RateLimitError,
    ValidationError,
    NotFoundError,
    PermissionDeniedError,
    APIConnectionError,
    APITimeoutError,
)

try:
    morph.edit.file(path="src/app.py", instruction="...", code_edit="...")
except RateLimitError:
    ...  # back off and retry
except MorphError as err:
    ...  # everything else
```

## Configuration

`Morph(...)` and `AsyncMorph(...)` accept:

| Argument | Default | Purpose |
| --- | --- | --- |
| `api_key` | `MORPH_API_KEY` | API key. Required. |
| `timeout` | `30.0` | Per-request timeout in seconds. |
| `max_retries` | `3` | Retries on transient failures. |
| `debug` | `False` | Log requests to stderr (`MORPH_DEBUG=1`). |

## License

MIT
