Metadata-Version: 2.4
Name: brainy_deco
Version: 0.1.2
Summary: A decorator that turns Python functions into LLM calls
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# smart_function / brainy_deco

A Python decorator that converts any function into an LLM call — powered by its **docstring** and **type signature**. Callable arguments are automatically exposed as **tools** the LLM can invoke during reasoning.

## How it works

```python
@smart_func("gemma4")
def translate(text: str, target_language: str) -> str:
    """Translate the given text into the target language."""
```

When you call `translate("Hello!", "French")`, the decorator:

1. **Inspects** the function signature and docstring
2. **Detects** any callable arguments and registers them as LLM-callable tools
3. **Assembles** a structured prompt describing the task, arguments, and available tools
4. **Runs** a conversation loop with the configured LLM backend:
   - If the LLM emits `_llm_call = {"tool": "...", "args": [...]}` → calls the tool, feeds result back
   - If the LLM emits `_llm_return = <value>` → extracts and returns the Python literal
5. **Returns** the final result (parsed with `ast.literal_eval`, no arbitrary code execution)

## Requirements

- Python ≥ 3.10
- [httpx](https://www.python-httpx.org/) (`pip install httpx`)
- A running [Ollama](https://ollama.com/) instance (default) or an OpenAI-compatible API

## Installation

```bash
pip install -e .          # editable install
# or
uv pip install -e .
```

## Quick start

### Basic usage — Ollama (default, local)

```python
from brainy_deco import smart_func

@smart_func("gemma4")          # model name is the only required argument
def translate(text: str, target_language: str) -> str:
    """Translate the given text into the target language."""

print(translate("Hello, world!", "French"))
# → 'Bonjour, le monde !'
```

### Sentiment analysis (returns a float)

```python
@smart_func("gemma4")
def sentiment_score(text: str) -> float:
    """
    Analyse the sentiment of the text.
    Return a float between -1.0 (very negative) and 1.0 (very positive).
    """

print(sentiment_score("This is amazing!"))   # → 0.95
```

### Structured extraction (returns a dict)

```python
@smart_func("gemma4")
def extract_person(bio: str) -> dict:
    """
    Extract personal information from a biography.
    Return a dict with keys: name, age, occupation. Use None for unknown fields.
    """

print(extract_person("Marie Curie was a 66-year-old physicist."))
# → {'name': 'Marie Curie', 'age': 66, 'occupation': 'physicist'}
```

### Callable parameter as a tool (LLM-driven callback)

If any argument passed at call time is **callable**, it is automatically
exposed to the LLM as a tool the LLM may invoke zero or more times.

```python
from typing import Callable

@smart_func("gemma4")
def recommend_action(ticker: str, get_price: Callable) -> str:
    """
    Fetch the current price of ticker using get_price, then recommend
    one of 'buy', 'hold', or 'sell' with a one-sentence reason.
    Return a string like: "hold – trading near fair value."
    """

def live_price(ticker: str) -> float:
    """Look up the real-time stock price for ticker."""
    ...   # your actual implementation

print(recommend_action("AAPL", live_price))
# LLM calls live_price("AAPL"), receives 175.5, then returns the recommendation.
# → 'hold – the stock is trading around its recent average price.'
```

### `tools=` dict — tools declared at decoration time

Use `tools=` to inject callables that are always available to the LLM,
**independent of call-site arguments**. Useful when the helpers are
implementation details that callers shouldn't need to know about.

```python
def _km_to_miles(km: float) -> float:
    """Convert kilometres to miles."""
    return km * 0.621371

def _miles_to_km(miles: float) -> float:
    """Convert miles to kilometres."""
    return miles / 0.621371

@smart_func(
    "gemma4",
    tools={
        "km_to_miles": _km_to_miles,
        "miles_to_km": _miles_to_km,
    },
)
def convert_units(value: float, from_unit: str, to_unit: str) -> float:
    """
    Convert value from from_unit to to_unit (supported: km, miles).
    Use the provided tools to perform the conversion.
    """

print(convert_units(100.0, "km", "miles"))   # → 62.1371
```

Tools from `tools=` and auto-detected callable arguments are **merged** into a
single registry. On name collision, `tools=` entries take precedence.

### OpenAI-compatible endpoint

```python
@smart_func(
    "gpt-4o-mini",
    backend_type="openai",
    api_key="sk-...",
    base_url="https://api.openai.com/v1",   # optional, this is the default
)
def summarize(article: str, max_words: int = 100) -> str:
    """Summarize the article in at most max_words words."""
```

### Custom backend instance

```python
from brainy_deco import smart_func
from brainy_deco.backends import OpenAIBackend

backend = OpenAIBackend(
    model="gemma4",
    base_url="http://localhost:1234/v1",   # LM Studio, etc.
    temperature=0.2,
)

@smart_func(backend=backend)
def classify(text: str, categories: list) -> str:
    """Classify the text into one of the given categories."""
```

## Decorator parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `model` | `str` | `"gemma4"` | LLM model name |
| `backend` | `LLMBackend` | `None` | Pre-built backend (overrides all other connection params) |
| `backend_type` | `str` | `"ollama"` | `"ollama"` or `"openai"` |
| `base_url` | `str` | `"http://localhost:11434"` | API base URL |
| `api_key` | `str` | `None` | API key (OpenAI-compatible) |
| `tools` | `dict[str, Callable]` | `None` | Tools always available to the LLM, by name |
| `debug` | `bool` | `False` | Log prompt and raw LLM responses |
| `**backend_kwargs` | | | Forwarded to backend (e.g. `temperature`, `timeout`) |

## LLM response protocol

The decorator uses a two-phase protocol:

### Phase 1 — Tool call (zero or more times)

```
_llm_call = {"tool": "<tool_name>", "args": [<arg1>, <arg2>, ...]}
```

The callable is invoked with the given args and the result is fed back into
the conversation as a new message. The loop continues until phase 2.

### Phase 2 — Final return (exactly once)

```
_llm_return = <Python literal>
```

Extracted with `ast.literal_eval` (safe — no arbitrary code execution).  
Supported types: `str`, `int`, `float`, `bool`, `None`, `list`, `dict`, `tuple`.

The loop is capped at **20 tool-call rounds** to prevent runaway conversations.

## Running the demo

```bash
# Make sure Ollama is running and the model is pulled
ollama pull gemma4
uv run examples/demo.py
```

## Running tests

```bash
uv run pytest tests/ -v
```

## Project layout

```
smart_function/
├── brainy_deco/
│   ├── __init__.py      # public API
│   ├── decorator.py     # @smart_func decorator + tool-calling loop
│   ├── prompt.py        # prompt builder (system + user messages)
│   ├── parser.py        # response parser (FinalResult / ToolCall)
│   └── backends.py      # OllamaBackend, OpenAIBackend
├── examples/
│   └── demo.py          # runnable examples (all 6 scenarios)
├── tests/
│   └── test_smart_function.py
└── pyproject.toml
```