# Tutorial: REST API actions with @WebService

Bind any REST endpoint to an LLM-callable action through annotations alone — no `OkHttpClient` plumbing, no manual JSON parsing, no per-call retry loops in your Role. The framework's `WebServiceExecutor` handles the wire-format work; your Role just declares **what** to call.

## Prerequisites

- [Installation](../start/installation.md)
- `OPENAI_API_KEY` (LLM)
- A bearer token in `OPENWEATHER_API_KEY` and `ALERTS_API_TOKEN` for the example endpoints (or substitute your own)

## The role

Full source lives at `tnsai-integration/src/main/java/com/tnsai/integration/scop/examples/WeatherRole.java` and is exercised by `WeatherRoleAnnotationRoundTripTest`. The shape:

```java
@RoleSpec(
    name = "WeatherAgent",
    description = "Looks up current weather and forecasts via OpenWeatherMap.",
    llm = @LLMSpec(
        provider = LLMSpec.Provider.OPENAI,
        model = "gpt-4o-mini",
        temperature = 0.2f)
)
public class WeatherRole {
    // ... actions below
}
```

## Pattern 1 — GET with query params + bearer auth

The canonical "fetch from a public API" shape. Each parameter value is URL-encoded and appended as a query parameter; the bearer token is read from the env var at request time.

```java
@ActionSpec(
    type = ActionType.WEB_SERVICE,
    description = "Current weather conditions for a city, by name.",
    webService = @WebService(
        endpoint = "https://api.openweathermap.org/data/2.5/weather",
        method = HttpMethod.GET,
        paramType = ParamType.QUERY,
        auth = AuthType.BEARER,
        authTokenEnv = "OPENWEATHER_API_KEY",
        timeout = 5000,
        retryCount = 2,
        retryBackoffMs = 500
    )
)
public String getCurrentWeather(
    String q,
    String units
) {
    return null; // body unused — WebServiceExecutor calls the API
}
```

The framework's `WebServiceExecutor` resolves this annotation at dispatch time, builds the URL (`?q=Istanbul&units=metric`), injects `Authorization: Bearer $OPENWEATHER_API_KEY`, and parses the JSON response back into the action's return type.

## Pattern 2 — GET with path templating

Single-brace `{paramName}` syntax substitutes path variables before the request leaves the JVM. Remaining params still go to the query string when `paramType = QUERY`.

```java
@ActionSpec(
    type = ActionType.WEB_SERVICE,
    description = "N-day forecast for a city by OpenWeatherMap city id.",
    webService = @WebService(
        endpoint = "https://api.openweathermap.org/data/2.5/forecast/{cityId}",
        method = HttpMethod.GET,
        paramType = ParamType.QUERY,
        auth = AuthType.BEARER,
        authTokenEnv = "OPENWEATHER_API_KEY",
        timeout = 7500
    )
)
public String getForecastByCityId(
    String cityId,
    int cnt
) {
    return null;
}
```

Note: the framework uses **single-brace `{cityId}`** syntax, not `${cityId}`. The Javadoc on `@WebService.endpoint()` is the authority.

## Pattern 3 — POST with body, custom headers, separate auth token

`paramType = BODY` serialises parameter values as a JSON request body. Custom `@Header` entries pass through verbatim (useful for upstream-required identifiers). Each action gets its own `authTokenEnv`, so different services can use different bearer tokens within the same role.

```java
@ActionSpec(
    type = ActionType.WEB_SERVICE,
    description = "Subscribe a user to weather-threshold alerts.",
    webService = @WebService(
        endpoint = "https://api.example.com/alerts/subscribe",
        method = HttpMethod.POST,
        paramType = ParamType.BODY,
        contentType = "application/json",
        accept = "application/json",
        auth = AuthType.BEARER,
        authTokenEnv = "ALERTS_API_TOKEN",
        headers = {
            @Header(key = "X-Source",        value = "tnsai-weather-agent"),
            @Header(key = "Accept-Language", value = "en-US")
        },
        timeout = 10000
    )
)
public String subscribeToAlerts(
    String city,
    int thresholdC
) {
    return null;
}
```

## Method bodies are deliberately stubs

`return null;` (or any trivial return) is a documented framework convention. `WebServiceExecutor` never invokes the method body — the Java signature exists purely so the parameter list and the LLM-tool schema can be generated from it. The actual return value comes from JSON deserialization of the HTTP response.

## How SCOP routes these

Every `@ActionSpec(type = WEB_SERVICE)` on a registered Role is dispatched by `WebServiceExecutor` (in `tnsai-core/actions/executors/`). The framework's `ActionExecutor` routing table wires `WEB_SERVICE → WebServiceExecutor` at construction (`ActionExecutor.java:177`); no consumer code needs to register it. `SCOPBridge.executeAction(role, name, params)` is the entry point for SCOP-driven invocations.

## Wiring the role into an Agent

```java
Agent agent = AgentBuilder.create()
    .role(new WeatherRole())
    .llm(new OpenAIClient("gpt-4o-mini"))
    .build();

Object current = agent.executeAction(
    "getCurrentWeather",
    Map.of("q", "Istanbul", "units", "metric"));
```

No tool registration call needed — the framework reads `@WebService` reflectively from the role's methods, populates `ActionMetadata.webServiceConfig()`, and the executor pulls every field at dispatch time.

## Authentication today

| `AuthType` | What it sends | Required env var(s) |
|---|---|---|
| `NO_AUTH` | Nothing | — |
| `BEARER` | `Authorization: Bearer <token>` | `authTokenEnv` |
| `BASIC` | `Authorization: Basic <base64(user:pass)>` | `authUsernameEnv` + `authPasswordEnv` |
| `API_KEY` | `<apiKeyHeader>: <key>` (default header `X-API-Key`) | `authTokenEnv` (+ optional `apiKeyHeader` to override the default header name) |

`API_KEY` example — upstream expects `X-Goog-Api-Key: …` instead of a standard `Authorization` header:

```java
@ActionSpec(
    type = ActionType.WEB_SERVICE,
    description = "Search Google Books",
    webService = @WebService(
        endpoint = "https://www.googleapis.com/books/v1/volumes",
        method = HttpMethod.GET,
        paramType = ParamType.QUERY,
        auth = AuthType.API_KEY,
        authTokenEnv = "GOOGLE_BOOKS_API_KEY",
        apiKeyHeader = "X-Goog-Api-Key"
    )
)
public BookSearchResult searchBooks(String query) {
    return null;
}
```

## Retry, timeout, and headers

| Annotation field | Default | When to override |
|---|---|---|
| `timeout` | 30000 ms | Slow upstreams, real-time dashboards |
| `retryCount` | 0 | Idempotent reads on flaky networks |
| `retryBackoffMs` | 1000 ms | Adjust backoff cadence |
| `followRedirects` | true | Set `false` for strict single-hop POSTs |
| `contentType` | `application/json` | Use `application/x-www-form-urlencoded` etc. |
| `accept` | `application/json` | When the upstream returns XML / CSV |
| `headers` | `{}` | Per-request static headers (auth tokens go in `authTokenEnv`, not here) |

## When to use `@WebService` vs an LLM action with tools

Both surface as LLM-callable actions, but they solve different problems:

| | `@ActionSpec(type = WEB_SERVICE)` | `@ActionSpec(type = LLM)` + agent toolkits |
|---|---|---|
| **Caller** | LLM picks the action via tool-call; framework calls the endpoint | LLM picks the action, then picks one of the agent's `@Tool` methods to call |
| **Wire format** | HTTP endpoint declared in annotation | Method-level `@Tool` schema on the registered POJO |
| **Auth** | Per-action env-var injection | Per-toolkit env var (read by the POJO) |
| **Best for** | Wrapping a known REST endpoint with typed parameters | Open-ended action where the LLM should choose among several tools |
| **Setup cost** | Just the annotation | Register a POJO toolkit via `AgentBuilder.builtInTools(...)` or `.toolPojos(...)` |

If you have one specific REST endpoint to call with typed parameters, use `@WebService`. If the LLM should pick *which* tool to call from a set you've registered with the agent, use an LLM action and let the dispatcher handle tool routing.

## Reference

- [SCOP Bridge](../integrate/scop.md) — the integration layer reference
- `@WebService` annotation — `tnsai-core/src/main/java/com/tnsai/annotations/WebService.java`
- `WebServiceExecutor` — `tnsai-core/src/main/java/com/tnsai/actions/executors/WebServiceExecutor.java`
- `WebServiceConfig` record — `tnsai-core/src/main/java/com/tnsai/metadata/WebServiceConfig.java` (the grouped accessor used by executors)
- `WeatherRoleAnnotationRoundTripTest` — `tnsai-integration/src/test/java/com/tnsai/integration/scop/examples/` (annotation-contract sentinel)
