Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# REST API

**The REST API is experimental and subject to change.**

The Blackgeorge REST API exposes all framework functionality via HTTP endpoints.

## Quick Start

```bash
# Install dependencies
uv pip install -e .[dev]

# Start the server
uv run uvicorn blackgeorge.api:create_app --factory --reload

# Or with host/port
uv run uvicorn blackgeorge.api:create_app --factory --host 0.0.0.0 --port 8000
```

The API will be available at `http://localhost:8000`

Interactive documentation is available at:
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`

## Authentication

Currently no authentication. For production use, add API keys or OAuth2.

## Core Concepts

### Workers
Single AI agents with tools, instructions, and models.

### Workforces
Multi-agent coordination in managed or collaborate mode.

### Runs
Individual job executions with status tracking and pause/resume.

## Endpoints

### Health

#### GET /health
Check API health status.

```bash
curl http://localhost:8000/health
```

Response:
```json
{
"status": "healthy",
"version": "0.1.0"
}
```

### Workers

#### POST /api/v1/workers
Register a new worker.

```bash
curl -X POST http://localhost:8000/api/v1/workers \
-H "Content-Type: application/json" \
-d '{
"name": "Researcher",
"model": "openai/gpt-4",
"instructions": "You are a researcher."
}'
```

#### GET /api/v1/workers
List all registered workers.

```bash
curl http://localhost:8000/api/v1/workers
```

#### GET /api/v1/workers/{name}
Get specific worker details.

```bash
curl http://localhost:8000/api/v1/workers/Researcher
```

#### DELETE /api/v1/workers/{name}
Unregister a worker.

```bash
curl -X DELETE http://localhost:8000/api/v1/workers/Researcher
```

### Workforces

#### POST /api/v1/workforces
Create a workforce.

```bash
curl -X POST http://localhost:8000/api/v1/workforces \
-H "Content-Type: application/json" \
-d '{
"name": "MyTeam",
"workers": ["Researcher", "Writer"],
"mode": "managed"
}'
```

#### GET /api/v1/workforces
List all workforces.

```bash
curl http://localhost:8000/api/v1/workforces
```

#### GET /api/v1/workforces/{name}
Get specific workforce details.

```bash
curl http://localhost:8000/api/v1/workforces/MyTeam
```

#### DELETE /api/v1/workforces/{name}
Unregister a workforce.

```bash
curl -X DELETE http://localhost:8000/api/v1/workforces/MyTeam
```

### Runs

#### POST /api/v1/runs/worker/{name}
Execute a job with a worker.

```bash
curl -X POST http://localhost:8000/api/v1/runs/worker/Researcher \
-H "Content-Type: application/json" \
-d '{
"input": "Summarize AI trends in 2024",
"expected_output": "A concise summary"
}'
```

Response:
```json
{
"run_id": "abc123",
"status": "completed",
"content": "AI trends in 2024 include...",
"data": null,
"pending_action": null,
"metrics": {},
"errors": [],
"messages": [],
"tool_calls": []
}
```

#### POST /api/v1/runs/workforce/{name}
Execute a job with a workforce.

```bash
curl -X POST http://localhost:8000/api/v1/runs/workforce/MyTeam \
-H "Content-Type: application/json" \
-d '{
"input": "Write a research report on quantum computing"
}'
```

#### POST /api/v1/runs/{run_id}/resume
Resume a paused run.

```bash
curl -X POST http://localhost:8000/api/v1/runs/{run_id}/resume \
-H "Content-Type: application/json" \
-d '{
"decision": true
}'
```

### Status

#### GET /api/v1/runs/{run_id}
Get run status and results.

```bash
curl http://localhost:8000/api/v1/runs/{run_id}
```

#### GET /api/v1/runs
List all runs with optional filtering.

```bash
# List all runs
curl http://localhost:8000/api/v1/runs

# Filter by status
curl "http://localhost:8000/api/v1/runs?status=completed"

# With pagination
curl "http://localhost:8000/api/v1/runs?limit=10&offset=20"
```

#### GET /api/v1/runs/{run_id}/events
Get events for a run.

```bash
curl http://localhost:8000/api/v1/runs/{run_id}/events
```

## Configuration

Configure the API via environment variables with the `BLACKGEORGE_API_` prefix:

```bash
export BLACKGEORGE_API_DEFAULT_MODEL="openai/gpt-4"
export BLACKGEORGE_API_STORAGE_DIR="/data/blackgeorge"
export BLACKGEORGE_API_CORS_ORIGINS='["http://localhost:3000"]'
```

## Development

### Running Tests

```bash
# Run all tests
uv run pytest

# Run API tests only
uv run pytest tests/api/

# Run with coverage
uv run pytest tests/api/ --cov=blackgeorge.api
```

### Type Checking

```bash
uv run mypy src/blackgeorge/api/
```

### Linting

```bash
uv run ruff check src/blackgeorge/api/
uv run ruff format src/blackgeorge/api/
```
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ authors = [{ name = "Dušsan Jolović", email = "jolovic@pm.me" }]
urls = { Repository = "https://github.com/jolovicdev/blackgeorge" }
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"instructor[litellm]>=1.11.0",
"litellm>=1.80.0",
"pydantic>=2.8.0",
"pydantic-settings>=2.6.0",
"uvicorn[standard]>=0.32.0",
]

[dependency-groups]
dev = [
"httpx>=0.28.0",
"mypy>=1.11.0",
"mkdocs",
"mkdocs-material",
Expand Down Expand Up @@ -43,6 +47,7 @@ line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
per-file-ignores = { "src/blackgeorge/api/routes/*.py" = ["B008"] }

[build-system]
requires = ["uv_build>=0.9.11,<0.10.0"]
Expand Down
3 changes: 3 additions & 0 deletions src/blackgeorge/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from blackgeorge.api.main import create_app

__all__ = ["create_app"]
26 changes: 26 additions & 0 deletions src/blackgeorge/api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class APIConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="BLACKGEORGE_API_")

title: str = "Blackgeorge API"
version: str = "0.1.0"
cors_origins: list[str] = ["*"]

default_model: str = "openai/gpt-4"
default_temperature: float | None = None
max_tokens: int | None = None
default_stream: bool = False

storage_dir: str = ".blackgeorge"


_config: APIConfig | None = None


def get_config() -> APIConfig:
global _config
if _config is None:
_config = APIConfig()
return _config
28 changes: 28 additions & 0 deletions src/blackgeorge/api/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from collections.abc import AsyncGenerator

from blackgeorge.api.config import APIConfig
from blackgeorge.desk import Desk

_config: APIConfig | None = None
_desk: Desk | None = None


def get_config() -> APIConfig:
global _config
if _config is None:
_config = APIConfig()
return _config


async def get_desk() -> AsyncGenerator[Desk, None]:
global _desk
if _desk is None:
config = get_config()
_desk = Desk(
model=config.default_model,
storage_dir=config.storage_dir,
temperature=config.default_temperature,
max_tokens=config.max_tokens,
stream=config.default_stream,
)
yield _desk
26 changes: 26 additions & 0 deletions src/blackgeorge/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class APIException(Exception):
def __init__(self, status_code: int, detail: str, errors: list[str] | None = None):
self.status_code = status_code
self.detail = detail
self.errors = errors or []
super().__init__(detail)


class WorkerNotFoundError(APIException):
def __init__(self, worker_name: str):
super().__init__(404, f"Worker '{worker_name}' not found")


class WorkforceNotFoundError(APIException):
def __init__(self, workforce_name: str):
super().__init__(404, f"Workforce '{workforce_name}' not found")


class RunNotFoundError(APIException):
def __init__(self, run_id: str):
super().__init__(404, f"Run '{run_id}' not found")


class InvalidResumeError(APIException):
def __init__(self, reason: str):
super().__init__(400, f"Cannot resume run: {reason}")
Loading