Skip to content

Commit d45797a

Browse files
committed
Add HTTP Generator docs
1 parent 484cc0d commit d45797a

File tree

3 files changed

+176
-1
lines changed

3 files changed

+176
-1
lines changed

docs/api/generator.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
::: rigging.generator
2+
::: rigging.generator.http
23
::: rigging.generator.vllm_
34
::: rigging.generator.transformers_

docs/topics/generators.md

+154
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,160 @@ params by using the associated [`.with_()`][rigging.chat.ChatPipeline.with_] fun
190190
print(chat.last.content)
191191
```
192192

193+
## HTTP Generator
194+
195+
The [`HTTPGenerator`][rigging.generator.http.HTTPGenerator] allows you to wrap any HTTP endpoint as a generator,
196+
making it easy to integrate external LLMs or AI services into your Rigging pipelines. It works by
197+
defining a specification that maps message content into HTTP requests and parses responses back into
198+
messages.
199+
200+
The specification is assigned to the [`.spec`][rigging.generator.http.HTTPGenerator.spec] field on the generator,
201+
and can be applied as a Python dictionary, JSON string, YAML string, or base64 encoded JSON/YAML string.
202+
203+
This flexibility allows you to easily share and reuse specifications across different parts of your application.
204+
205+
```python
206+
import rigging as rg
207+
208+
spec = r"""
209+
request:
210+
url: "https://{{ model }}.crucible.dreadnode.io/submit"
211+
headers:
212+
"X-Api-Key": "{{ api_key }}"
213+
"Content-Type": "application/json"
214+
transforms:
215+
- type: "json"
216+
pattern: {
217+
"data": "$content"
218+
}
219+
response:
220+
transforms:
221+
- type: "jsonpath"
222+
pattern: $.flag,output,message
223+
"""
224+
225+
crucible = rg.get_generator("http!spanglish,api_key=<key>") # (1)
226+
crucible.spec = spec
227+
228+
chat = await crucible.chat("A flag please").run()
229+
230+
print(chat.conversation)
231+
# [user]: A flag please
232+
#
233+
# [assistant]: Una bandera, por favor.
234+
```
235+
236+
1. Were are using the `.model` field on the generator to carry our crucible challenge
237+
238+
!!! tip "Saving schemas"
239+
240+
Encoded YAML is the default storage when an HTTP generator is serialized to an indentifier using
241+
[`to_identifier`][rigging.generator.Generator.to_identifier]. This also means that when we save
242+
our chats to storage, they maintain their http specification.
243+
244+
```py
245+
print(crucible.to_identifier())
246+
# http!spanglish,spec=eyJyZXF1ZXN0Ijp7InVyb...
247+
```
248+
249+
### Specification
250+
251+
The [specification (`HTTPSpec`)][rigging.generator.http.HTTPSpec] controls how messages are transformed around HTTP interactions. It supports:
252+
253+
- Template-based URLs
254+
- Template-based header generation
255+
- Configurable timeouts and HTTP methods
256+
- Status code validation
257+
- Flexible body transformations for both the request and response
258+
259+
When building requests, the following [context variables (`RequestTransformContext`)][rigging.generator.http.RequestTransformContext]
260+
are available in your transform patterns:
261+
262+
- `role` - Role of the last message (user/assistant/system)
263+
- `content` - Content of the last message
264+
- `all_content` - Concatenated content of all messages
265+
- `messages` - List of all message objects
266+
- `params` - Generation parameters (temperature, max_tokens, etc.)
267+
- `api_key` - API key from the generator
268+
- `model` - Model identifier from the generator
269+
270+
For both request and response transform chains, the previous result of each transform is
271+
provided to the next transform via any of `data`, `output`, `result`, or `body`.
272+
273+
### Transforms
274+
275+
The HTTP generator supports different types of transforms for both request building and response parsing.
276+
Each serves a specific purpose and has its own pattern syntax.
277+
278+
!!! tip "Transform Chaining"
279+
280+
Transforms are applied in sequence, with each transform's output becoming the input for the next.
281+
This allows you to build complex processing pipelines:
282+
283+
```yaml
284+
transforms:
285+
- type: "jsonpath"
286+
pattern: "$.data" # Extract data object
287+
- type: "jinja"
288+
pattern: "{{ result | tojson }}" # Convert to string
289+
- type: "regex"
290+
pattern: "message: (.*)" # Extract specific field
291+
```
292+
293+
**Jinja (request + response)**
294+
295+
The `jinja` transform type provides full Jinja2 template syntax. Access context variables directly
296+
and use Jinja2 filters and control structures.
297+
298+
```yaml
299+
transforms:
300+
- type: "jinja"
301+
pattern: >
302+
{
303+
"content": "{{ all_content }}",
304+
"timestamp": "{{ now() }}",
305+
{% if params.temperature > 0.5 %}
306+
"mode": "creative"
307+
{% endif %}
308+
}
309+
```
310+
311+
**JSON (request only)**
312+
313+
The `json` transform type lets you build JSON request bodies using a template object. Use `$` prefix
314+
to reference context variables, with dot notation for nested access:
315+
316+
```yaml
317+
transforms:
318+
- type: "json"
319+
pattern: {
320+
"messages": "$messages",
321+
"temperature": "$params.temperature",
322+
"content": "$content",
323+
"static_field": "hello"
324+
}
325+
```
326+
327+
**JSONPath (response only)**
328+
329+
The `jsonpath` transform type uses [JSONPath](https://github.com/h2non/jsonpath-ng) expressions to extract data from JSON responses:
330+
331+
```yaml
332+
transforms:
333+
- type: "jsonpath"
334+
pattern: "$.choices[0].message.content"
335+
```
336+
337+
**Regex (response only)**
338+
339+
The `regex` transform type uses regular expressions to extract content from text responses:
340+
341+
```yaml
342+
transforms:
343+
- type: "regex"
344+
pattern: "<output>(.*?)</output>"
345+
```
346+
193347
## Writing a Generator
194348

195349
All generators should inherit from the [`Generator`][rigging.generator.Generator] base class, and

rigging/generator/http.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ class RequestTransformContext(BaseModel):
8888

8989
class TransformStep(BaseModel, t.Generic[TransformT]):
9090
type: TransformT
91+
"""Type of transform to apply."""
92+
9193
pattern: str | dict[str, t.Any]
94+
"""Pattern to use for the transform."""
9295

9396

9497
class RequestSpec(BaseModel):
@@ -99,10 +102,19 @@ class RequestSpec(BaseModel):
99102
"""
100103

101104
url: str
105+
"""URL to send the request to (Jinja templates supported)."""
106+
102107
method: str = "POST"
108+
"""HTTP method to use for the request."""
109+
103110
headers: dict[str, str] = {}
111+
"""Headers to include in the request (Jinja templates supported)."""
112+
104113
timeout: int | None = None
114+
"""Timeout in seconds for the request."""
115+
105116
transforms: list[TransformStep[InputTransform]] = Field(min_length=1)
117+
"""Transforms to apply to the messages to build the request body."""
106118

107119

108120
class ResponseSpec(BaseModel):
@@ -113,14 +125,20 @@ class ResponseSpec(BaseModel):
113125
"""
114126

115127
valid_status_codes: list[int] = [200]
128+
"""Valid status codes for the response."""
129+
116130
transforms: list[TransformStep[OutputTransform]]
131+
"""Transforms to apply to the response body to generate the message content."""
117132

118133

119134
class HTTPSpec(BaseModel):
120135
"""Defines how to build requests and parse responses for the HTTPGenerator."""
121136

122137
request: RequestSpec
138+
"""Specification for building the request."""
139+
123140
response: ResponseSpec | None = None
141+
"""Specification for parsing the response."""
124142

125143
@raise_as(ProcessingError, "Error while transforming input")
126144
def make_request_body(self, context: RequestTransformContext) -> str:
@@ -191,7 +209,9 @@ def parse_response_body(self, data: str) -> str:
191209
matches = [match.value for match in jsonpath_expr.find(result)]
192210
if len(matches) == 0:
193211
raise Exception(f"No matches found for JSONPath: {transform.pattern} from {result}")
194-
result = json.dumps(matches) if len(matches) > 1 else json.dumps(matches[0])
212+
elif len(matches) == 1:
213+
matches = matches[0]
214+
result = matches if isinstance(matches, str) else json.dumps(matches)
195215

196216
elif transform.type == "regex":
197217
matches = re.findall(_to_str(transform.pattern), result)

0 commit comments

Comments
 (0)