Comprehensive documentation set for XWiki: - Home, Installation, Quick Start guides - Writing Handlers and LLM Router guides - Architecture docs (Overview, Message Pump, Thread Registry, Shared Backend) - Reference docs (Configuration, Handler Contract, CLI) - Hello World tutorial - Why XML rationale - Pandoc conversion scripts (bash + PowerShell) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
376 lines
8 KiB
Markdown
376 lines
8 KiB
Markdown
# Hello World Tutorial
|
|
|
|
Build a complete greeting agent from scratch. By the end, you'll understand payloads, handlers, configuration, and message flow.
|
|
|
|
## What We're Building
|
|
|
|
A greeting system with three components:
|
|
|
|
```
|
|
User Input → Greeter Agent → Shouter Tool → Output
|
|
│ │ │ │
|
|
│ │ │ │
|
|
"Alice" "Hello, "HELLO, Displayed
|
|
Alice!" ALICE!" to user
|
|
```
|
|
|
|
## Prerequisites
|
|
|
|
- Python 3.11+
|
|
- xml-pipeline installed (`pip install xml-pipeline[console]`)
|
|
|
|
## Step 1: Create Project Structure
|
|
|
|
```bash
|
|
mkdir hello-world
|
|
cd hello-world
|
|
mkdir -p config handlers
|
|
```
|
|
|
|
## Step 2: Define Payloads
|
|
|
|
Create `handlers/payloads.py`:
|
|
|
|
```python
|
|
from dataclasses import dataclass
|
|
from third_party.xmlable import xmlify
|
|
|
|
@xmlify
|
|
@dataclass
|
|
class Greeting:
|
|
"""Request to greet someone."""
|
|
name: str
|
|
|
|
@xmlify
|
|
@dataclass
|
|
class GreetingResponse:
|
|
"""A friendly greeting."""
|
|
message: str
|
|
|
|
@xmlify
|
|
@dataclass
|
|
class ShoutRequest:
|
|
"""Request to shout text."""
|
|
text: str
|
|
|
|
@xmlify
|
|
@dataclass
|
|
class ShoutResponse:
|
|
"""Shouted text."""
|
|
text: str
|
|
|
|
@xmlify
|
|
@dataclass
|
|
class ConsoleOutput:
|
|
"""Text to display."""
|
|
text: str
|
|
source: str = "system"
|
|
```
|
|
|
|
**What's happening:**
|
|
- `@xmlify` enables XML serialization
|
|
- `@dataclass` provides the fields
|
|
- Each class becomes a valid XML payload
|
|
|
|
## Step 3: Write Handlers
|
|
|
|
Create `handlers/greeter.py`:
|
|
|
|
```python
|
|
from xml_pipeline.message_bus.message_state import HandlerMetadata, HandlerResponse
|
|
from handlers.payloads import Greeting, GreetingResponse, ShoutRequest
|
|
|
|
async def handle_greeting(payload: Greeting, metadata: HandlerMetadata) -> HandlerResponse:
|
|
"""
|
|
Receive a greeting request, create a friendly message,
|
|
then forward to the shouter to make it exciting.
|
|
"""
|
|
# Create the greeting
|
|
message = f"Hello, {payload.name}! Welcome to xml-pipeline!"
|
|
|
|
# Forward to shouter (will come back to us? No - goes to output)
|
|
return HandlerResponse(
|
|
payload=ShoutRequest(text=message),
|
|
to="shouter",
|
|
)
|
|
```
|
|
|
|
Create `handlers/shouter.py`:
|
|
|
|
```python
|
|
from xml_pipeline.message_bus.message_state import HandlerMetadata, HandlerResponse
|
|
from handlers.payloads import ShoutRequest, ConsoleOutput
|
|
|
|
async def handle_shout(payload: ShoutRequest, metadata: HandlerMetadata) -> HandlerResponse:
|
|
"""
|
|
Take text and SHOUT IT!
|
|
Then send to console for display.
|
|
"""
|
|
shouted = payload.text.upper() + "!!!"
|
|
|
|
return HandlerResponse(
|
|
payload=ConsoleOutput(text=shouted, source="shouter"),
|
|
to="console-output",
|
|
)
|
|
```
|
|
|
|
Create `handlers/output.py`:
|
|
|
|
```python
|
|
from xml_pipeline.message_bus.message_state import HandlerMetadata
|
|
from handlers.payloads import ConsoleOutput
|
|
|
|
async def handle_output(payload: ConsoleOutput, metadata: HandlerMetadata) -> None:
|
|
"""
|
|
Display text to console.
|
|
Returns None to end the message chain.
|
|
"""
|
|
print(f"\n[{payload.source}] {payload.text}\n")
|
|
return None # Chain ends here
|
|
```
|
|
|
|
## Step 4: Configure the Organism
|
|
|
|
Create `config/organism.yaml`:
|
|
|
|
```yaml
|
|
organism:
|
|
name: hello-world
|
|
|
|
listeners:
|
|
# The greeter agent
|
|
- name: greeter
|
|
payload_class: handlers.payloads.Greeting
|
|
handler: handlers.greeter.handle_greeting
|
|
description: "Greets people by name"
|
|
peers:
|
|
- shouter
|
|
|
|
# The shouter tool
|
|
- name: shouter
|
|
payload_class: handlers.payloads.ShoutRequest
|
|
handler: handlers.shouter.handle_shout
|
|
description: "Makes text LOUD"
|
|
peers:
|
|
- console-output
|
|
|
|
# Output handler
|
|
- name: console-output
|
|
payload_class: handlers.payloads.ConsoleOutput
|
|
handler: handlers.output.handle_output
|
|
description: "Displays text to console"
|
|
```
|
|
|
|
## Step 5: Verify Configuration
|
|
|
|
```bash
|
|
xml-pipeline check config/organism.yaml
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
Config valid: hello-world
|
|
Listeners: 3
|
|
LLM backends: 0
|
|
```
|
|
|
|
## Step 6: Create Test Script
|
|
|
|
Create `test_greeting.py`:
|
|
|
|
```python
|
|
import asyncio
|
|
from xml_pipeline.message_bus.stream_pump import StreamPump
|
|
from xml_pipeline.config.loader import load_config
|
|
|
|
async def main():
|
|
# Load configuration
|
|
config = load_config("config/organism.yaml")
|
|
|
|
# Create and start the pump
|
|
pump = StreamPump(config)
|
|
await pump.start()
|
|
|
|
print("Organism started! Injecting greeting...")
|
|
|
|
# Create a greeting message
|
|
greeting_xml = b"""<?xml version="1.0"?>
|
|
<message xmlns="https://xml-pipeline.org/ns/envelope/v1">
|
|
<meta>
|
|
<from>test</from>
|
|
<to>greeter</to>
|
|
</meta>
|
|
<greeting>
|
|
<name>Alice</name>
|
|
</greeting>
|
|
</message>
|
|
"""
|
|
|
|
# Inject the message
|
|
await pump.inject(greeting_xml, from_id="test")
|
|
|
|
# Give it time to process
|
|
await asyncio.sleep(1)
|
|
|
|
# Shutdown
|
|
await pump.shutdown()
|
|
print("Done!")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
## Step 7: Run It
|
|
|
|
```bash
|
|
python test_greeting.py
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
Organism started! Injecting greeting...
|
|
|
|
[shouter] HELLO, ALICE! WELCOME TO XML-PIPELINE!!!!
|
|
|
|
Done!
|
|
```
|
|
|
|
## What Just Happened?
|
|
|
|
Let's trace the message flow:
|
|
|
|
### 1. Message Injection
|
|
|
|
```xml
|
|
<greeting>
|
|
<name>Alice</name>
|
|
</greeting>
|
|
```
|
|
|
|
Injected with `from=test`, `to=greeter`.
|
|
|
|
### 2. Pipeline Processing
|
|
|
|
```
|
|
Raw bytes
|
|
↓
|
|
repair_step → Valid XML tree
|
|
↓
|
|
c14n_step → Canonicalized
|
|
↓
|
|
envelope_valid → Matches envelope.xsd
|
|
↓
|
|
payload_extract → Extracts <greeting>
|
|
↓
|
|
thread_assign → UUID: abc-123
|
|
↓
|
|
xsd_validate → Matches Greeting schema
|
|
↓
|
|
deserialize → Greeting(name="Alice")
|
|
↓
|
|
routing → target: greeter
|
|
```
|
|
|
|
### 3. Handler Dispatch
|
|
|
|
```python
|
|
# greeter receives:
|
|
payload = Greeting(name="Alice")
|
|
metadata.thread_id = "abc-123"
|
|
metadata.from_id = "test"
|
|
```
|
|
|
|
### 4. Response Processing
|
|
|
|
Greeter returns:
|
|
```python
|
|
HandlerResponse(
|
|
payload=ShoutRequest(text="Hello, Alice!..."),
|
|
to="shouter",
|
|
)
|
|
```
|
|
|
|
System:
|
|
1. Validates `shouter` is in greeter's peers ✓
|
|
2. Serializes ShoutRequest to XML
|
|
3. Wraps in envelope with `from=greeter`
|
|
4. Re-injects into pipeline
|
|
|
|
### 5. Chain Continues
|
|
|
|
Shouter receives ShoutRequest, returns ConsoleOutput to `console-output`.
|
|
|
|
### 6. Chain Terminates
|
|
|
|
`handle_output` returns `None` → chain ends.
|
|
|
|
## Exercises
|
|
|
|
### Exercise 1: Add a Counter
|
|
|
|
Modify the shouter to count how many times it's been called:
|
|
|
|
```python
|
|
# Hint: Use a module-level variable (simple) or
|
|
# the context buffer (proper way)
|
|
```
|
|
|
|
### Exercise 2: Add Error Handling
|
|
|
|
What happens if someone sends an empty name? Add validation:
|
|
|
|
```python
|
|
async def handle_greeting(payload: Greeting, metadata: HandlerMetadata):
|
|
if not payload.name.strip():
|
|
return HandlerResponse(
|
|
payload=ConsoleOutput(text="Error: Name required", source="greeter"),
|
|
to="console-output",
|
|
)
|
|
# ... rest of handler
|
|
```
|
|
|
|
### Exercise 3: Make Greeter an LLM Agent
|
|
|
|
Convert greeter to use an LLM for creative greetings:
|
|
|
|
```python
|
|
from xml_pipeline.platform.llm_api import complete
|
|
|
|
async def handle_greeting(payload: Greeting, metadata: HandlerMetadata):
|
|
response = await complete(
|
|
model="grok-4.1",
|
|
messages=[
|
|
{"role": "system", "content": "Generate a creative, friendly greeting."},
|
|
{"role": "user", "content": f"Greet someone named {payload.name}"},
|
|
],
|
|
)
|
|
|
|
return HandlerResponse(
|
|
payload=ShoutRequest(text=response.content),
|
|
to="shouter",
|
|
)
|
|
```
|
|
|
|
Don't forget to add LLM configuration:
|
|
|
|
```yaml
|
|
llm:
|
|
backends:
|
|
- provider: xai
|
|
api_key_env: XAI_API_KEY
|
|
```
|
|
|
|
## Summary
|
|
|
|
You've learned:
|
|
- **Payloads**: Define messages with `@xmlify` dataclasses
|
|
- **Handlers**: Async functions that process and respond
|
|
- **Configuration**: Wire everything in organism.yaml
|
|
- **Message Flow**: How messages traverse the pipeline
|
|
- **Chaining**: Handlers forward to each other via `HandlerResponse`
|
|
|
|
## Next Steps
|
|
|
|
- [[Writing Handlers]] — More handler patterns
|
|
- [[Configuration]] — Full configuration reference
|
|
- [[Architecture Overview]] — Deep dive into internals
|