Metadata-Version: 2.4
Name: app-theforge-sdk
Version: 0.1.0
Summary: Composition library for AI prompt systems managed by The Forge. Imported as `forge_sdk`.
License: MIT
Project-URL: Homepage, https://github.com/app-theforge/the-forge
Project-URL: Repository, https://github.com/app-theforge/the-forge
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: pyyaml>=6.0
Requires-Dist: httpx>=0.28
Requires-Dist: supabase>=2.9
Requires-Dist: python-dotenv>=1.0
Provides-Extra: integration
Requires-Dist: fastapi>=0.115; extra == "integration"
Requires-Dist: python-jose[cryptography]>=3.3; extra == "integration"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: fastapi>=0.115; extra == "dev"
Requires-Dist: python-jose[cryptography]>=3.3; extra == "dev"
Requires-Dist: httpx>=0.28; extra == "dev"

# forge-sdk

Composition library for AI prompt systems managed by The Forge.

The base install gives you the prompt-composition modules (`compose`,
`PromptSource`, `DiskSource`, `SupabaseSource`, directive resolution,
knowledge loading). The optional `[integration]` extra adds the
primitives a connected app needs to talk to The Forge runtime.

## Install

```bash
pip install forge-sdk                 # prompt composition only
pip install forge-sdk[integration]    # + FastAPI + python-jose
```

## Prompt composition

```python
from forge_sdk import compose, ComposeRequest, DiskSource, SkillConfig

result = compose(
    ComposeRequest(
        config=SkillConfig(
            agent_slug="coach",
            skill_slug="warmup",
            knowledge_files=["drills.md"],
            user_sports=["tennis"],
        )
    ),
    source=DiskSource("./prompts"),
)

print(result.request.system)
print(result.request.knowledge)
```

The composer resolves `{{include:...}}` directives against the
configured `PromptSource` (disk for local dev, Supabase for prod) and
loads knowledge files filtered by user attributes.

## Integration submodule

`pip install forge-sdk[integration]` adds primitives connected apps
need to talk to The Forge:

- Bearer auth (FastAPI `Depends()` factory)
- HMAC JWT helpers for the token types Forge exchanges with apps:
  candidate (linked-user lookup) and event (orchestration callbacks,
  signing is SDK-test-only — apps just relay tokens minted by Forge)
- Async events client for POSTing run events back to Forge
- FastAPI router scaffold with pluggable app-supplied callbacks

### Minimal example

```python
from fastapi import FastAPI
from forge_sdk.integration import build_router, UserLookupResult

async def my_lookup(email: str) -> list[UserLookupResult]:
    docs = await db.users.find({"email": email}).to_list(50)
    return [
        UserLookupResult(
            user_id=str(d["_id"]),
            display_name=f"{d.get('first_name','')} {d.get('last_name','')}".strip(),
            created_at=d.get("created_at"),
        )
        for d in docs
    ]

app = FastAPI()
app.include_router(
    build_router(
        bearer_env="FORGE_TRAIN_BEARER",
        lookup_signing_key_env="FORGE_TRAIN_LOOKUP_SIGNING_KEY",
        lookup_users_by_email=my_lookup,
    ),
    prefix="/forge",
)
```

The SDK owns bearer auth, candidate-token signing, response shape, and
404-on-empty semantics. The app owns how to actually find users in its
own database — supply that as the `lookup_users_by_email` async
callback.

### Posting events

When Forge invokes the app's `/forge/run` endpoint, the payload includes
an `events_url` and a Forge-signed `event_token`. The app relays those
straight into `ForgeEventsClient` — it never signs event tokens itself.

```python
from forge_sdk.integration import ForgeEventsClient

async with ForgeEventsClient(
    events_url=f"{forge_url}/api/runs/{run_id}/events",
    event_token=event_token_from_forge_run_payload,
    raise_on_error=False,  # background tasks: log + continue
) as client:
    await client.post_event({"type": "started", "run_id": run_id})
```

### Required env vars

| Env var | Purpose | Where it's set |
|---|---|---|
| `FORGE_*_BEARER` | Bearer the app accepts on Forge → app calls | Both sides (Forge `project_secrets`, app `.env`) |
| `FORGE_*_LOOKUP_SIGNING_KEY` | HMAC key for candidate tokens (linked-user lookup) | Both sides, same value |
| `FORGE_*_EVENT_SIGNING_KEY` | HMAC key for event tokens (SDK-side testing only) | SDK tests; apps don't normally need it |

`EVENT_SIGNING_KEY` is for SDK-side testing only — apps don't sign
event tokens. Forge mints + signs event tokens and hands them to the
app via `/forge/run`; the app relays via `ForgeEventsClient(event_token=...)`.

The `*` slot is the app slug (e.g. `FORGE_TRAIN_BEARER`). Apps pick
their own prefix; the SDK doesn't hardcode names — env-var names are
passed into each helper, dependency, and the router builder.

### Lower-level primitives

If `build_router(...)` is too opinionated, the building blocks are
exported directly:

```python
from forge_sdk.integration import (
    bearer_dependency,         # FastAPI Depends() factory
    sign_candidate_token,      # + verify_candidate_token
    sign_event_token,          # + verify_event_token (SDK-test-only)
    ForgeEventsClient,         # async POSTs to Forge run-events endpoint
    require_bearer,            # env-var read with typed errors
    require_signing_key,       # env-var read with typed errors
)
```

All of these accept env-var names rather than raw secrets, so secrets
never sit in argument lists or get logged.
