Skip to content
Merged
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
149 changes: 149 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# agentflow-api (API server + CLI) — Engineering Guide

This file documents the **API server and CLI package** only (`10xscale-agentflow-cli`). For the
core framework see `agentflow/CLAUDE.md`; for the TS client, docs, or playground see their folders;
for the monorepo overview see the workspace-root `CLAUDE.md`.

- Package name (PyPI): `10xscale-agentflow-cli`
- Version: `0.3.2.9` (`pyproject.toml`) — note the CLI's own `CLI_VERSION` constant and
`agentflow_cli.__version__` both say `1.0.0`; these are out of sync (see Known Doc Drift).
- Requires: Python >= 3.12 · Status: `4 - Beta`
- Console entry point: `agentflow = agentflow_cli.cli.main:main`
- Depends on the core framework: `10xscale-agentflow>=0.7.0`.

## What this package is

It turns an Agentflow `CompiledGraph` into a production FastAPI service, plus a Typer CLI to
scaffold, run, build, test, and evaluate that service. You write a graph, point `agentflow.json`
at it, and `agentflow api` serves it over REST + WebSocket with auth, rate limiting, media
handling, checkpointer/thread management, and a memory store API.

## Package layout

Importable package: `agentflow_cli/`. Two halves:

| Path | What lives there |
|---|---|
| `agentflow_cli/cli/` | The Typer CLI. `main.py` (command definitions), `commands/` (one class per command: api, build, eval, init, skills, test, version), `core/` (config, output, validation), `constants.py`, `templates/` (project scaffolds: `dev/` minimal, `prod/` full) |
| `agentflow_cli/src/app/` | The FastAPI app. `main.py` + `loader.py` (build app from `agentflow.json`), `routers/` (graph, checkpointer, store, media, ping; a2a/a2ui present but not mounted), `core/auth/`, `core/config/`, `core/middleware/` (rate_limit, security_headers, request_limits), `tasks/`, `utils/`, `worker.py` |

Public exports from the package root (`from agentflow_cli import ...`): `BaseAuth`,
`SnowFlakeIdGenerator`, `ThreadNameGenerator`.

## CLI commands (verified against `cli/main.py`)

| Command | Purpose | Notable options |
|---|---|---|
| `agentflow api` | Start the API server | `--config/-c` (default `agentflow.json`), `--host/-H`, `--port/-p` (8000), `--reload/--no-reload`, `-v/-q` |
| `agentflow play` | Start the server and open the hosted playground | same as `api` |
| `agentflow init` | Interactively scaffold a project (questionary prompts pick dev vs production, auth, rate limit) | `--path/-p`, `--force/-f`. There is **no `--prod` flag**; setup type is chosen interactively |
| `agentflow build` | Generate a `Dockerfile` (and optionally `docker-compose.yml`) | `--output/-o`, `--python-version` (3.13), `--port`, `--docker-compose/--no-docker-compose`, `--service-name` |
| `agentflow eval` | Run agent evaluations; discovers `*_eval.py`/`eval_*.py`, runs cases (optionally `--parallel`), writes HTML+JSON to `eval_reports/` | `--output/-o`, `--no-report`, `--threshold/-t`, `--open`, `--parallel/-p`, `--max-concurrency/-c` |
| `agentflow test` | Run project tests via pytest (args after `--` forwarded verbatim) | `--coverage/-C`, `--html`, `-k`, path arg |
| `agentflow skills` | Install bundled Agentflow skills for Codex/Claude/GitHub | `--agent/-a`, `--path/-p`, `--force/-f`, `--all`, `--list/-l` |
| `agentflow version` | Show CLI + package version | reads `CLI_VERSION` constant + package version from `pyproject.toml` |

Defaults (from `cli/constants.py`): `DEFAULT_HOST="127.0.0.1"`, `DEFAULT_PORT=8000`,
`DEFAULT_CONFIG_FILE="agentflow.json"`.

## `agentflow.json` (the config contract)

Parsed by `agentflow_cli/src/app/core/config/graph_config.py`. Supported keys:

| Key | Meaning |
|---|---|
| `agent` (required) | `"module:attribute"` resolving to a `CompiledGraph`. The loader accepts a `CompiledGraph` object, a sync/async factory returning one, or a callable. |
| `env` | Path to a `.env` file, loaded at config-load time |
| `thread_name_generator` | `"module:attr"` -> a `ThreadNameGenerator` |
| `auth` | `null`, the string `"jwt"`, or `{"method": "custom", "path": "module:attr"}` |
| `authorization` | `"module:attr"` -> an `AuthorizationBackend` (RBAC / per-tool access) |
| `checkpointer` | `"module:attr"` -> a `BaseCheckpointer` |
| `injectq` | `"module:attr"` -> an InjectQ container |
| `store` | `"module:attr"` -> a `BaseStore` |
| `redis` | Redis URL string |
| `rate_limit` | Object (see below) |

`rate_limit` object: `enabled`, `requests` (default 100), `window` secs (60), `by` (`ip` |
`global`), `backend` (`memory` | `redis` | `custom`), `trusted_proxy_headers` (honour
`X-Forwarded-For` only when true), `exclude_paths`, `fail_open` (on backend error: allow vs deny),
and for redis backend a `redis` sub-object `{ "url", "prefix" }` (or shorthand URL string). For
`custom`, bind a `BaseRateLimitBackend` in InjectQ.

## Auth

- `"auth": "jwt"` requires `JWT_SECRET_KEY` and `JWT_ALGORITHM` in the environment (raises at
load if missing). JWT logic lives in `core/auth/jwt_auth.py`.
- `"auth": {"method": "custom", "path": "module:attr"}` loads your `BaseAuth` subclass
(`from agentflow_cli import BaseAuth`).
- Authorization (RBAC, per-tool) is separate: `core/auth/authorization.py`
(`AuthorizationBackend` / `DefaultAuthorizationBackend`), wired via the `authorization` key.

## HTTP + WebSocket surface (all under `/v1` except ping)

- **Graph** (`tags=["Graph"]`): `POST /v1/graph/invoke`, `POST /v1/graph/stream`,
`POST /v1/graph/stop`, `POST /v1/graph/setup`, `POST /v1/graph/fix`, `GET /v1/graph`,
`WS /v1/graph/ws`.
- **Checkpointer / threads**: `GET/POST /v1/threads`, `GET/DELETE /v1/threads/{thread_id}`,
`GET /v1/threads/{thread_id}/state`, `GET /v1/threads/{thread_id}/messages`,
`... /messages/{message_id}`.
- **Store (memory)**: `POST /v1/store/memories`, `/v1/store/memories/list`,
`/v1/store/memories/forget`, `/v1/store/memories/{memory_id}`, `POST /v1/store/search`.
- **Media / files** (`tags=["Files"]`): `POST /v1/files/upload`, `GET /v1/files/{file_id}`,
`/{file_id}/info`, `/{file_id}/url`, `GET /v1/config/multimodal`.
- **Ping**: `GET /ping`.

Routers are wired in `routers/setup_router.py` (`init_routes`). `a2a.py` and `a2ui.py` exist but
are **not** mounted there yet.

## Settings / environment

`core/config/settings.py` is a `pydantic-settings` `Settings` (with `extra="allow"`, so unknown
env vars are tolerated). Notable vars: `APP_NAME`, `APP_VERSION`, `MODE` (`development` |
`production`), `LOG_LEVEL`, `IS_DEBUG`, `MAX_REQUEST_SIZE` (10MB default), security headers
(`SECURITY_HEADERS_ENABLED`, `HSTS_*`, `FRAME_OPTIONS`, `CSP_POLICY`, ...), `ORIGINS` (CORS,
default `*` with a wildcard warning), `ALLOWED_HOST`, `ROOT_PATH`/`DOCS_PATH`/`REDOCS_PATH`,
`REDIS_URL`, `SENTRY_DSN`, `SNOWFLAKE_*` (epoch/node/worker/bit layout), `JWT_SECRET_KEY`/
`JWT_ALGORITHM`, `OTEL_ENABLED`/`OTEL_SERVICE_NAME`/`OTEL_EXPORTER_OTLP_ENDPOINT`/`OTEL_LEVEL`.
In production: set `MODE=production`, `IS_DEBUG=false`, a non-`*` `ORIGINS`, and a strong
`JWT_SECRET_KEY`.

## Optional extras (`pyproject.toml`)

`sentry`, `firebase`, `snowflakekit`, `redis`, `jwt`, `media` (document text extraction via
`textxtract`), `gcloud` (Cloud Logging), `otel` (includes FastAPI instrumentation + OTLP exporter).

## Development workflow

```bash
# from this folder (agentflow-api/); a .venv is present
.venv/bin/python -m pytest # tests in tests/
agentflow init # scaffold (interactive)
agentflow api --reload # dev server on 127.0.0.1:8000
agentflow play # server + hosted playground
agentflow build --docker-compose # Dockerfile + compose
ruff check . && ruff format .
```

- Tests in `tests/`; `pytest` config and ruff/bandit are in `pyproject.toml`. Templates under
`cli/templates/{dev,prod}` are excluded from lint/type/bandit (they are emitted code, not lib).
- The `prod` template is the reference for a real project: it scaffolds `graph/` (agent, state,
tools, validators, thread_name_generator), `auth/`, `evals/`, and `tests/`.

## Known doc drift (do not trust without checking)

- **Version is inconsistent.** `pyproject.toml` = `0.3.2.9`, but `CLI_VERSION` constant and
`agentflow_cli.__version__` = `1.0.0`. `agentflow version` prints both the (hardcoded) CLI
version and the pyproject version, so it shows `1.0.0` and `0.3.2.9` side by side.
- **README shows `agentflow init --prod`** — that flag does not exist. `init` is interactive and
only accepts `--path` / `--force`.
- **`api`/`play` help text claims default host `0.0.0.0`** but `DEFAULT_HOST` is `127.0.0.1`.
- **"Pyagenity" branding leftovers.** The CLI app help, `agentflow_cli.__init__` docstring, the
`version` banner, and several router docstrings still say "Pyagenity" (the framework's former
name). Cosmetic but pervasive; rename to Agentflow when touching those files.
- **a2a / a2ui routers are not mounted.** Don't document a2a HTTP endpoints as live until
`setup_router.init_routes` includes them.
- **`pyproject.toml` URLs** point at `github.com/10xHub/agentflow-cli` and
`agentflow-cli.readthedocs.io`; confirm these are canonical vs the core's `agentflow.10xscale.ai`.
- The workspace-root `CLAUDE.md` lists only `init/api/play/build` and an older `agentflow.json`
shape; the real CLI has `eval/test/skills/version` too and the config supports `rate_limit`,
`thread_name_generator`, and `authorization`.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,8 @@ pip install "10xscale-agentflow-cli[media]"
### Initialize a New Project

```bash
# Create project structure
# Create project structure (interactive: prompts for dev vs production, auth, rate limiting)
agentflow init

# Or with production config
agentflow init --prod
```

### Start Development Server
Expand Down Expand Up @@ -85,12 +82,9 @@ For detailed command documentation, see the [CLI Guide](./docs/cli-guide.md).
Initialize a new project with configuration and sample graph.

```bash
# Basic initialization
# Basic initialization (interactive prompts choose dev vs production setup)
agentflow init

# With production config (pyproject.toml, pre-commit hooks)
agentflow init --prod

# Custom directory
agentflow init --path ./my-project

Expand Down
7 changes: 5 additions & 2 deletions agentflow_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Pyagenity API - A Python API framework For Pyagenity Graphs."""
"""Agentflow API - A Python API framework for Agentflow graphs."""

from agentflow_cli.cli.constants import CLI_VERSION as __version__


__version__ = "1.0.0"
__author__ = "Shudipto Trafder"
__email__ = "shudiptotrafder@gmail.com"

Expand All @@ -17,4 +19,5 @@
"BaseAuth",
"SnowFlakeIdGenerator",
"ThreadNameGenerator",
"__version__",
]
7 changes: 5 additions & 2 deletions agentflow_cli/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Pyagenity CLI package."""
"""Agentflow CLI package."""

__version__ = "1.0.0"
from agentflow_cli.cli.constants import CLI_VERSION as __version__


__all__ = ["__version__"]
4 changes: 2 additions & 2 deletions agentflow_cli/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def handle_error(self, error: Exception) -> int:
self.logger.error("Command failed: %s", error)

# Import here to avoid circular imports
from agentflow_cli.cli.exceptions import PyagenityCLIError
from agentflow_cli.cli.exceptions import AgentflowCLIError

if isinstance(error, PyagenityCLIError):
if isinstance(error, AgentflowCLIError):
self.output.error(error.message)
return error.exit_code

Expand Down
2 changes: 1 addition & 1 deletion agentflow_cli/cli/commands/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


class APICommand(BaseCommand):
"""Command to start the Pyagenity API server."""
"""Command to start the Agentflow API server."""

_PLAYGROUND_WAIT_TIMEOUT_SECONDS = 30.0
_PLAYGROUND_WAIT_INTERVAL_SECONDS = 0.25
Expand Down
8 changes: 2 additions & 6 deletions agentflow_cli/cli/commands/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,7 @@ async def _run_one(
# Report merging
# ------------------------------------------------------------------

def _merge_reports(
self, reports: list[EvalReport], base_config: Any = None
) -> EvalReport:
def _merge_reports(self, reports: list[EvalReport], base_config: Any = None) -> EvalReport:
if len(reports) == 1:
return reports[0]

Expand Down Expand Up @@ -680,9 +678,7 @@ def execute( # noqa: PLR0912, PLR0915

# Build per-eval-set config map from pending before results are consumed
group_configs: dict[str, Any] = {
pc.eval_set_id: pc.config
for pc in pending
if isinstance(pc, _PendingCase)
pc.eval_set_id: pc.config for pc in pending if isinstance(pc, _PendingCase)
}

# 7. Group by eval_set_id → one EvalReport per set
Expand Down
2 changes: 1 addition & 1 deletion agentflow_cli/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def execute(self, **kwargs: Any) -> int:
# Print banner
self.output.print_banner(
"Version",
"Show pyagenity CLI and package version info",
"Show Agentflow CLI and package version info",
color="green",
)

Expand Down
27 changes: 25 additions & 2 deletions agentflow_cli/cli/constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
"""CLI constants and configuration values."""

import tomllib
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _pkg_version
from pathlib import Path
from typing import Final


# Version information
CLI_VERSION: Final[str] = "1.0.0"
_PACKAGE_NAME: Final[str] = "10xscale-agentflow-cli"


def _resolve_version() -> str:
"""Resolve the package version from installed metadata, falling back to pyproject.toml.

Single source of truth: the installed distribution's version. When running from a
source checkout that is not installed, read ``pyproject.toml`` instead.
"""
try:
return _pkg_version(_PACKAGE_NAME)
except PackageNotFoundError:
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
try:
with pyproject.open("rb") as f:
return tomllib.load(f).get("project", {}).get("version", "unknown")
except (OSError, tomllib.TOMLDecodeError):
return "unknown"


# Version information (single-sourced from package metadata / pyproject.toml)
CLI_VERSION: Final[str] = _resolve_version()

# Default configuration values
DEFAULT_HOST: Final[str] = "127.0.0.1"
Expand Down
2 changes: 1 addition & 1 deletion agentflow_cli/cli/core/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Configuration management for the Pyagenity CLI."""
"""Configuration management for the Agentflow CLI."""

from __future__ import annotations

Expand Down
18 changes: 9 additions & 9 deletions agentflow_cli/cli/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Custom exceptions for the Pyagenity CLI."""
"""Custom exceptions for the Agentflow CLI."""


class PyagenityCLIError(Exception):
"""Base exception for all Pyagenity CLI errors."""
class AgentflowCLIError(Exception):
"""Base exception for all Agentflow CLI errors."""

def __init__(self, message: str, exit_code: int = 1) -> None:
"""Initialize the exception with a message and exit code.
Expand All @@ -16,7 +16,7 @@ def __init__(self, message: str, exit_code: int = 1) -> None:
self.exit_code = exit_code


class ConfigurationError(PyagenityCLIError):
class ConfigurationError(AgentflowCLIError):
"""Raised when there are configuration-related errors."""

def __init__(self, message: str, config_path: str | None = None) -> None:
Expand All @@ -30,7 +30,7 @@ def __init__(self, message: str, config_path: str | None = None) -> None:
self.config_path = config_path


class ValidationError(PyagenityCLIError):
class ValidationError(AgentflowCLIError):
"""Raised when input validation fails."""

def __init__(self, message: str, field: str | None = None) -> None:
Expand All @@ -44,7 +44,7 @@ def __init__(self, message: str, field: str | None = None) -> None:
self.field = field


class FileOperationError(PyagenityCLIError):
class FileOperationError(AgentflowCLIError):
"""Raised when file operations fail."""

def __init__(self, message: str, file_path: str | None = None) -> None:
Expand All @@ -58,7 +58,7 @@ def __init__(self, message: str, file_path: str | None = None) -> None:
self.file_path = file_path


class TemplateError(PyagenityCLIError):
class TemplateError(AgentflowCLIError):
"""Raised when template operations fail."""

def __init__(self, message: str, template_name: str | None = None) -> None:
Expand All @@ -72,7 +72,7 @@ def __init__(self, message: str, template_name: str | None = None) -> None:
self.template_name = template_name


class ServerError(PyagenityCLIError):
class ServerError(AgentflowCLIError):
"""Raised when server operations fail."""

def __init__(self, message: str, host: str | None = None, port: int | None = None) -> None:
Expand All @@ -88,7 +88,7 @@ def __init__(self, message: str, host: str | None = None, port: int | None = Non
self.port = port


class DockerError(PyagenityCLIError):
class DockerError(AgentflowCLIError):
"""Raised when Docker-related operations fail."""

def __init__(self, message: str, dockerfile_path: str | None = None) -> None:
Expand Down
2 changes: 1 addition & 1 deletion agentflow_cli/cli/logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Logging configuration for the Pyagenity CLI."""
"""Logging configuration for the Agentflow CLI."""

import logging
import sys
Expand Down
Loading
Loading