Metadata-Version: 2.5
Name: yoaiagent
Version: 0.2.3
Summary: Provider-agnostic Python AI agent framework with Bring Your Own Provider architecture
Author-email: Shekh Saheb Ali <sahebali9277071@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: pydantic<3.0,>=2.0
Provides-Extra: all
Requires-Dist: anthropic<1.0,>=0.30; extra == 'all'
Requires-Dist: google-genai<2.0,>=1.0; extra == 'all'
Requires-Dist: openai<2.0,>=1.0; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: anthropic<1.0,>=0.30; extra == 'anthropic'
Provides-Extra: cli
Requires-Dist: rich>=13.0; extra == 'cli'
Provides-Extra: config
Requires-Dist: python-dotenv>=1.0; extra == 'config'
Requires-Dist: pyyaml>=6.0; extra == 'config'
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: gemini
Requires-Dist: google-genai<2.0,>=1.0; extra == 'gemini'
Provides-Extra: openai
Requires-Dist: openai<2.0,>=1.0; extra == 'openai'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.0; extra == 'otel'
Description-Content-Type: text/markdown

# YoAI Agent

**Provider-agnostic Python AI agent framework with Bring Your Own Provider architecture.**

Write your agent code once, run it against OpenAI, Anthropic, Gemini, Ollama, OpenRouter, vLLM, or any OpenAI-compatible endpoint — without changing a line of agent code.

## Installation

```bash
pip install yoaiagent
```

With provider SDKs:

```bash
pip install yoaiagent[openai]       # Native OpenAI
pip install yoaiagent[anthropic]    # Native Anthropic
pip install yoaiagent[gemini]       # Native Google Gemini
pip install yoaiagent[all]          # All providers
pip install yoaiagent[config]       # YAML/TOML config + .env loading
pip install yoaiagent[otel]         # OpenTelemetry tracing
pip install yoaiagent[cli]          # CLI with rich output
```

## Quickstart

```python
from yoaiagent import Agent, LLM

llm = LLM(
    provider="openai-compatible",
    base_url="http://localhost:11434/v1",
    api_key="ollama",
    model="llama3.2",
)

agent = Agent(
    model=llm,
    instructions="You are a helpful AI assistant.",
)

result = agent.run("Explain quantum computing simply.")
print(result.output)
```

## Built-in Tools

10 ready-to-use tools included — no extra install needed:

```python
from yoaiagent import Agent, LLM, ALL_TOOLS

llm = LLM(provider="openai-compatible", base_url="http://localhost:11434/v1", api_key="ollama", model="llama3.2")

agent = Agent(
    model=llm,
    instructions="You are a coding assistant.",
    tools=ALL_TOOLS,
)

result = agent.run("Read the file config.json and summarize it")
print(result.output)
```

| Tool | Description |
|------|-------------|
| `read_file` | Read a file's contents with line numbers |
| `write_file` | Create or overwrite a file |
| `edit_file` | Replace text in a file |
| `list_files` | List files in a directory |
| `search_files` | Search for text inside files (grep) |
| `shell` | Execute a shell command ⚠️ dangerous |
| `get_repo_context` | Get git repo overview |
| `web_fetch` | Fetch and extract text from a URL |
| `context_summary` | Summarize long text |
| `plan` | Create numbered task plans |

Select specific tools:

```python
from yoaiagent.builtin_tools.coder import read_file, write_file, shell
from yoaiagent.builtin_tools.web import web_fetch

agent = Agent(model=llm, tools=[read_file, write_file, shell, web_fetch])
```

## Providers

### OpenAI

```python
llm = LLM(provider="openai", api_key="sk-...", model="gpt-5")
```

### Anthropic

```python
llm = LLM(provider="anthropic", api_key="sk-ant-...", model="claude-sonnet-4-20250514")
```

### Google Gemini

```python
llm = LLM(provider="gemini", api_key="AIza...", model="gemini-2.0-flash")
```

### OpenAI-Compatible (OpenRouter, Ollama, vLLM, etc.)

```python
# OpenRouter
llm = LLM(
    provider="openai-compatible",
    base_url="https://openrouter.ai/api/v1",
    api_key="your-key",
    model="meta-llama/llama-3.1-8b-instruct",
)

# Ollama (local)
llm = LLM(
    provider="openai-compatible",
    base_url="http://localhost:11434/v1",
    api_key="ollama",
    model="llama3.2",
)

# vLLM
llm = LLM(
    provider="openai-compatible",
    base_url="http://localhost:8080/v1",
    api_key="token",
    model="meta-llama/Llama-3.1-8B-Instruct",
)

# Custom gateway with headers
llm = LLM(
    provider="openai-compatible",
    base_url="https://ai.company.internal/v1",
    api_key="key",
    model="internal-model",
    headers={"X-Tenant-ID": "acme-corp"},
)
```

### Environment Variables

```bash
export YOAI_PROVIDER=openai
export YOAI_API_KEY=sk-...
export YOAI_MODEL=gpt-5
export YOAI_BASE_URL=https://api.openai.com/v1
```

```python
llm = LLM.from_env()
```

## Custom Tools

```python
from yoaiagent import Agent, LLM, tool

@tool
def calculator(a: float, b: float) -> float:
    """Add two numbers."""
    return a + b

@tool
def get_weather(city: str) -> str:
    """Get current weather for a city."""
    return f"Sunny, 25°C in {city}"

# Mark dangerous tools (requires user confirmation)
@tool(dangerous=True)
def shell(command: str) -> str:
    """Run a shell command."""
    import subprocess
    return subprocess.run(command, shell=True, capture_output=True, text=True).stdout

agent = Agent(
    model=llm,
    instructions="You are a helpful assistant.",
    tools=[calculator, get_weather],
)

result = agent.run("What is 123 + 456?")
print(result.output)
```

## Middleware

Intercept agent lifecycle for logging, budgeting, and safety:

```python
from yoaiagent import (
    Agent, LLM,
    TokenBudgetMiddleware,
    RateLimitMiddleware,
    CircuitBreakerMiddleware,
    ToolConfirmationMiddleware,
    StructuredLoggingHook,
    ConsoleLogger,
)

agent = Agent(
    model=llm,
    instructions="You are helpful.",
    tools=ALL_TOOLS,
    middleware=[
        ConsoleLogger(),                              # Print tool calls
        TokenBudgetMiddleware(max_total_tokens=50_000),  # Cap token usage
        RateLimitMiddleware(max_rpm=60),              # Throttle requests
        CircuitBreakerMiddleware(failure_threshold=3), # Stop on failures
        ToolConfirmationMiddleware(auto_approve=["read_file", "list_files"]),  # Confirm dangerous tools
    ],
)
```

### Available Middleware

| Middleware | Purpose |
|------------|---------|
| `ConsoleLogger` | Print tool calls to console |
| `TokenBudgetMiddleware` | Stop when token/cost limit exceeded |
| `RateLimitMiddleware` | Throttle to prevent rate limit hits |
| `CircuitBreakerMiddleware` | Stop calling LLM after repeated failures |
| `ToolConfirmationMiddleware` | Prompt before running dangerous tools |
| `StructuredLoggingHook` | JSON logs with correlation IDs |
| `OpenTelemetryMiddleware` | Export traces to Jaeger/Zipkin/Datadog |

## Streaming

```python
import asyncio
from yoaiagent import Agent, LLM

async def main():
    llm = LLM(provider="openai-compatible", base_url="http://localhost:11434/v1", api_key="ollama", model="llama3.2")
    agent = Agent(model=llm, instructions="You are a storyteller.")

    async for event in agent.astream("Tell me a short story"):
        if event.type == "text_delta":
            print(event.delta, end="", flush=True)
        elif event.type == "tool_call_started":
            print(f"\n[Using {event.tool_name}]")

asyncio.run(main())
```

## Structured Output

```python
from pydantic import BaseModel
from yoaiagent import Agent, LLM

class UserInfo(BaseModel):
    name: str
    age: int

llm = LLM(provider="openai-compatible", base_url="http://localhost:11434/v1", api_key="ollama", model="llama3.2")
agent = Agent(model=llm, instructions="Extract info.")

result = agent.run("John is 30 years old.", response_model=UserInfo)
print(result.output.name)  # "John"
print(result.output.age)   # 30
```

## Memory

### In-Memory (Process Only)

```python
from yoaiagent import Agent, LLM, InMemory

llm = LLM(provider="openai-compatible", base_url="http://localhost:11434/v1", api_key="ollama", model="llama3.2")
memory = InMemory()

agent = Agent(model=llm, instructions="You are helpful.", memory=memory)

agent.run("My name is Alice.")
result = agent.run("What is my name?")
print(result.output)  # "Your name is Alice."
```

### SQLite (Persistent)

```python
from yoaiagent import Agent, LLM, SQLiteMemory

memory = SQLiteMemory(db_path="~/.yoaiagent/memory.db")
agent = Agent(model=llm, memory=memory)

# Survives restarts
agent.run("My name is Alice.")
# ... restart your app ...
result = agent.run("What is my name?")
print(result.output)  # "Your name is Alice."
```

## Multi-Agent

```python
from yoaiagent import Agent, LLM

researcher = Agent(name="researcher", model=llm, instructions="Research assistant.")
writer = Agent(name="writer", model=llm, instructions="Write articles.")

# Make researcher available as a tool
writer.add_tool(researcher.as_tool())

result = writer.run("Write about AI.")
```

## Workflows

```python
from yoaiagent import Agent, LLM, Workflow

researcher = Agent(name="researcher", model=llm, instructions="Gather facts.")
writer = Agent(name="writer", model=llm, instructions="Write content.")
reviewer = Agent(name="reviewer", model=llm, instructions="Review for quality.")

workflow = Workflow()
workflow.add_node("research", researcher)
workflow.add_node("write", writer)
workflow.add_node("review", reviewer)

workflow.connect("research", "write")
workflow.connect("write", "review")

results = workflow.run("History of the internet")
```

## Configuration

### Config File

Create `yoaiagent.yaml` in your project root:

```yaml
llm:
  provider: openai-compatible
  model: llama3
  base_url: http://localhost:11434/v1
  api_key: ollama
  timeout: 60.0
  max_retries: 3
```

Then load it:

```python
llm = LLM.from_env()  # Reads config file + env vars
```

### Config Precedence

```
Direct code kwargs → Environment variables → Config file → .env → Defaults
```

## Custom Providers

```python
from yoaiagent import register_provider, BaseModel, ProviderCapabilities, LLMConfig, Message, RunResult

class MyProvider(BaseModel):
    provider = "my-provider"
    capabilities = ProviderCapabilities(supports_streaming=True)

    def __init__(self, config: LLMConfig):
        self.model = config.model
        # Initialize your HTTP client or SDK here

    async def generate(self, messages, **kwargs):
        # Call your API
        pass

    async def stream(self, messages, **kwargs):
        # Stream from your API
        pass

register_provider("my-provider", MyProvider)
llm = LLM(provider="my-provider", model="my-model", api_key="key")
```

## CLI

```bash
yoai providers     # List registered providers
yoai doctor        # Diagnose configuration issues
yoai version       # Show version
```

## Architecture

```
Agent
  ↓
LLM (config)
  ↓
Model Interface (BaseModel)
  ↓
Provider Adapter (OpenAICompatibleModel, OpenAIModel, AnthropicModel, GeminiModel)
  ↓
HTTP / SDK
```

The Agent communicates only with the common `BaseModel` interface. Provider adapters translate between internal messages and provider-specific formats. The agent code never changes when switching providers.

## License

MIT
