Metadata-Version: 2.4
Name: innerloop
Version: 0.0.1.dev9
Summary: LLM in a loop with tools, MCP, sessions, and structured outputs.
Project-URL: Homepage, https://github.com/botassembly/innerloop
Project-URL: Documentation, https://botassembly.org/innerloop
Project-URL: Repository, https://github.com/botassembly/innerloop
Project-URL: Issues, https://github.com/botassembly/innerloop/issues
Project-URL: Changelog, https://botassembly.org/innerloop/changelog/
Author-email: Ian Maurer <imaurer@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agent,ai,anthropic,automation,claude,codex,devtool,gemini,llm,openai,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: aiofiles>=24.1
Requires-Dist: anthropic>=0.40.0
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: google-generativeai>=0.8.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: openai>=1.50.0
Requires-Dist: pydantic>=2.10.0
Requires-Dist: trafilatura>=1.6
Provides-Extra: dev
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest-mock>=3.11.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# InnerLoop

[![PyPI](https://img.shields.io/pypi/v/innerloop.svg)](https://pypi.org/project/innerloop/)
[![Python](https://img.shields.io/pypi/pyversions/innerloop.svg)](https://pypi.org/project/innerloop/)
[![License](https://img.shields.io/github/license/botassembly/innerloop.svg)](LICENSE)

**Pure Python SDK for LLM agent loops.**

```bash
pip install innerloop
```

## One-liner

```python
from innerloop import run

response = run("What is 2+2?", model="openai/gpt-4o-mini")
print(response.text)  # "4"
```

## Tools are just functions

```python
from innerloop import Loop, tool

@tool
def get_weather(city: str) -> str:
    """Get weather for a city."""
    return f"Weather in {city}: 72°F"

loop = Loop(model="anthropic/claude-sonnet-4", tools=[get_weather])
response = loop.run("What's the weather in NYC?")
```

## Any provider, same API

```python
loop = Loop(model="anthropic/claude-sonnet-4")
loop = Loop(model="openai/gpt-4o")
loop = Loop(model="google/gemini-2.0-flash")
loop = Loop(model="openrouter/meta-llama/llama-3.1-8b-instruct:free")  # Free
loop = Loop(model="ollama/llama3")  # Local
```

## System prompt

```python
loop = Loop(
    model="anthropic/claude-sonnet-4",
    system="You are a helpful coding assistant. Be concise.",
)
```

## Structured output with Pydantic

```python
from pydantic import BaseModel
from innerloop import Loop

class City(BaseModel):
    name: str
    country: str
    population: int

loop = Loop(model="openai/gpt-4o")
response = loop.run("Tell me about Tokyo", response_format=City)
print(response.output.model_dump())
# {'name': 'Tokyo', 'country': 'Japan', 'population': 13929286}
```

If validation fails, it automatically retries (up to 3 times).

## Sessions without a database

```python
loop = Loop(model="anthropic/claude-sonnet-4")

with loop.session() as ask:
    ask("Remember: the secret word is 'banana'")
    response = ask("What's the secret word?")

print(response.session_id)  # "20251207144323-SA9MWJ"
```

Conversations save to `~/.local/share/innerloop/sessions/`. Resume anytime:

```python
loop = Loop(model="...", session="20251207144323-SA9MWJ")
```

## Streaming

```python
from innerloop import Loop, TextEvent

loop = Loop(model="anthropic/claude-sonnet-4")

for event in loop.stream("Write a poem"):
    if isinstance(event, TextEvent):
        print(event.text, end="", flush=True)
```

## Async

```python
import asyncio
from innerloop import Loop

async def main():
    loop = Loop(model="anthropic/claude-sonnet-4")
    response = await loop.arun("Hello!")

    async for event in loop.astream("Write a story"):
        ...

asyncio.run(main())
```

## Built-in tools

```python
from innerloop import Loop, SAFE_FS_TOOLS, FS_TOOLS, WEB_TOOLS

# SAFE_FS_TOOLS: read, glob, ls, grep (read-only)
# FS_TOOLS: read, write, edit, glob, ls, grep
# WEB_TOOLS: fetch, download, search

loop = Loop(model="...", tools=FS_TOOLS)
loop.run("Read main.py and add type hints")
```

Constrain bash for safety:

```python
from innerloop import bash

safe_bash = bash(allow={"make": "Run builds"}, deny=["rm -rf", "sudo"])
loop = Loop(model="...", tools=[safe_bash])
```

## Response details

```python
response = loop.run("What's 2+2?")

response.text           # "4"
response.usage          # Usage(input_tokens=12, output_tokens=5)
response.tool_results   # List of tool calls and outputs
response.session_id     # "20251207144323-SA9MWJ"
```

## Documentation

- [Getting Started](docs/getting-started.md)
- [Custom Tools](docs/custom-tools.md)
- [Structured Output](docs/structured-output.md)
- [Sessions](docs/sessions.md)
- [Streaming](docs/streaming.md)
- [Providers](docs/providers.md)
- [Built-in Tools](docs/builtin-tools.md)
- [Bash Tool](docs/bash.md)
- [Configuration](docs/configuration.md)

## License

MIT
