Metadata-Version: 2.4
Name: lughus
Version: 0.15.0
Summary: Micro-framework for building A2A agents with LiteLLM — agent loop, tool registry, A2A gateway.
Author: hdg-zero
License-Expression: MIT
Project-URL: Homepage, https://github.com/hdg-zero/lughus
Project-URL: Repository, https://github.com/hdg-zero/lughus
Project-URL: Issues, https://github.com/hdg-zero/lughus/issues
Project-URL: Changelog, https://github.com/hdg-zero/lughus/blob/main/CHANGELOG.md
Keywords: agent,a2a,llm,litellm,agentic,tool-calling
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: litellm<2.0,>=1.0
Requires-Dist: python-dotenv
Requires-Dist: jsonschema>=4.0
Requires-Dist: opentelemetry-api>=1.20
Provides-Extra: server
Requires-Dist: a2a-sdk<1.0,>=0.3.0; extra == "server"
Requires-Dist: fastapi; extra == "server"
Requires-Dist: uvicorn[standard]; extra == "server"
Requires-Dist: starlette; extra == "server"
Requires-Dist: sse-starlette; extra == "server"
Provides-Extra: otel
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == "otel"
Provides-Extra: all
Requires-Dist: lughus[otel,server]; extra == "all"
Provides-Extra: dev
Requires-Dist: lughus[all]; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-mock>=3.12; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="docs/logo.svg" width="360" alt="lughus logo" />
</p>

<p align="center">
  <a href="https://pypi.org/project/lughus/"><img src="https://img.shields.io/pypi/v/lughus.svg?color=blue" alt="PyPI version" /></a>
  <a href="https://pypi.org/project/lughus/"><img src="https://img.shields.io/pypi/pyversions/lughus.svg" alt="Supported Python versions" /></a>
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
</p>

# lughus

Micro-framework for building [A2A](https://google.github.io/A2A/) agents with [LiteLLM](https://github.com/BerriAI/litellm). Register tools, run an agentic loop, get a result. No graphs, no runners, no magic.

## Install

```bash
pip install lughus              # Core (litellm, python-dotenv, jsonschema)
pip install lughus[server]      # + FastAPI, uvicorn, a2a-sdk
pip install lughus[all]         # Everything
```

## Quick Start

A complete agent in one script. Register a tool, call `agent_loop`, get the LLM's response:

```python
import asyncio
import json

from lughus import ToolRegistry, agent_loop
from lughus.testing import MockLLM


# 1. Create a tool registry and register a tool
registry = ToolRegistry()


@registry.tool(
    "greet",
    "Greet a user by name.",
    {
        "type": "object",
        "properties": {
            "name": {"type": "string", "description": "Name to greet"},
        },
        "required": ["name"],
        "additionalProperties": False,
    },
)
def greet(*, name: str, state) -> str:
    return json.dumps({"greeting": f"Hello, {name}!"})


# 2. Create an LLM (MockLLM for offline testing, LLM for production)
llm = MockLLM(
    [
        # Turn 1: LLM calls the greet tool
        [{"name": "greet", "arguments": {"name": "World"}, "id": "call_1"}],
        # Turn 2: LLM produces a text response (ends the loop)
        "Hello, World!",
    ]
)


# 3. Run the agent loop
async def main():
    result = await agent_loop(
        llm,
        system="You are a greeting assistant. Use the greet tool.",
        context="Say hello to World",
        registry=registry,
        tool_names=["greet"],
        state=None,
    )
    print(result)  # "Hello, World!"
    print(f"{result.iterations} iterations, {result.total_tokens} tokens")


asyncio.run(main())
```

For production, swap `MockLLM` for a real LLM:

```python
from lughus import LLM

llm = LLM(model="openai/gpt-4o", max_output_tokens=16384)
```

## Features

- **`agent_loop()`** -- iterates LLM + tools until a text response, with parallel tool execution
- **`agent_loop_stream()`** -- same, but yields text chunks as the LLM generates them
- **`ToolRegistry`** -- `@registry.tool()` decorator for sync and async Python functions
- **`BaseGateway`** -- A2A `AgentExecutor` (message extraction, artifact handling)
- **`LLM`** -- thin wrapper around `litellm.acompletion()`, supports 100+ providers
- **`build_app()` / `serve()`** -- A2A ASGI app + uvicorn in one call
- **Governance** -- deterministic tool policies, scoped permissions, human-in-the-loop approvals
- **Observability** -- native OpenTelemetry traces and metrics on every request

## Configuration

All configuration is via environment variables. Key settings:

| Variable | Default | Description |
|---|---|---|
| `AGENT_MODEL` | *(required)* | LiteLLM model string (e.g. `openai/gpt-4o`) |
| `MAX_OUTPUT_TOKENS` | `16384` | Max output tokens per LLM call |
| `HOST` / `PORT` | `0.0.0.0` / `8080` | Server listen address |
| `LUGHUS_ENV` | `development` | Set `production` for strict startup validation |
| `API_BEARER_TOKEN` | *(not set)* | Bearer token for non-health routes |

Provider routing is automatic via LiteLLM:

```bash
export AGENT_MODEL="openai/gpt-4o"       && export OPENAI_API_KEY="sk-..."
export AGENT_MODEL="anthropic/claude-sonnet-4-20250514" && export ANTHROPIC_API_KEY="sk-ant-..."
export AGENT_MODEL="gemini/gemini-2.5-flash" && export GEMINI_API_KEY="..."
```

## Scaffold a New Agent

```bash
lughus new my_agent
cd my_agent && pip install -e ".[dev]" && pytest -q
python -m my_agent  # starts A2A server on :8080
```

## Governance

Tools can declare risk levels, required scopes, and approval workflows. The policy engine evaluates actions deterministically before execution -- prompt instructions are never used as access controls. See [docs/guides/agentic-design.md](docs/guides/agentic-design.md) for agentic design rules.

```python
from lughus import ToolRegistry, ToolRisk, ToolEffect

registry = ToolRegistry()

@registry.tool(
    "deploy",
    "Deploy to production.",
    {"type": "object", "properties": {"service": {"type": "string"}}, "required": ["service"]},
    risk=ToolRisk.CRITICAL,
    effects=frozenset([ToolEffect.WRITE, ToolEffect.IRREVERSIBLE]),
    requires_approval=True,
)
def deploy(*, service: str, state) -> str:
    return json.dumps({"status": "deployed"})
```

## Links

- [CHANGELOG](CHANGELOG.md)
- [CONTRIBUTING](CONTRIBUTING.md)
- [Architecture decisions](docs/architecture/)
- [Agentic design rules](docs/guides/agentic-design.md)
- [Guarantees](docs/guarantees.md)
- [LiteLLM providers](https://docs.litellm.ai/docs/providers)

## License

MIT -- see [LICENSE](LICENSE).
