# OmniLink Python Library — Getting Started Guide

OmniLink is a Python toolkit for building AI-powered automation agents. It
provides two complementary systems:

- **Local Tools** — Run AI controllers locally (games, robots, any task) with
  one-credit cloud orchestration via `ToolRunner`.
- **Command Engine** — Match natural-language commands to Python handlers with
  variable extraction, bidirectional messaging, and HTTP/MQTT transport.

---

## Table of Contents

1. [Installation](#installation)
2. [Quick start — local tool](#quick-start--local-tool)
3. [ToolRunner reference](#toolrunner-reference)
4. [Core concepts — command engine](#core-concepts--command-engine)
5. [Quick start — engine only](#quick-start--engine-only)
6. [Templates and types](#templates-and-types)
7. [Writing handlers](#writing-handlers)
8. [Bidirectional messaging](#bidirectional-messaging)
9. [Connecting over HTTP](#connecting-over-http)
10. [Connecting over MQTT](#connecting-over-mqtt)
11. [Pluggable use cases](#pluggable-use-cases)
12. [REST API client](#rest-api-client)
13. [Environment variable reference](#environment-variable-reference)
14. [Running the built-in examples](#running-the-built-in-examples)
15. [Running the tests](#running-the-tests)

---

## Installation

```bash
# From PyPI (when published)
pip install omnilink

# Or install from source (editable)
git clone https://github.com/omnilink/omnilink
pip install -e omnilink/omnilink-lib
```

Python **3.9 or later** is required. All common dependencies (`paho-mqtt`,
`requests`) are installed automatically.

---

## Quick start — local tool

A **local tool** is a Python controller that runs on your machine and is
orchestrated by OmniLink's cloud AI. The cloud agent triggers a tool call
(e.g. `make_move`), which hands control to your local code. Your code runs
the actual logic — game AI, robot navigation, data pipeline — at full speed
with zero per-action API calls.

### How it works

```
┌─────────────────┐    1 API call     ┌──────────────────┐
│  OmniLink Cloud │ ───────────────── │  Your ToolRunner │
│  (Chat + Memory)│   "Call make_move"│  (local Python)  │
└─────────────────┘                   └────────┬─────────┘
                                               │ polls state,
                                               │ sends actions
                                      ┌────────▼─────────┐
                                      │   Target System   │
                                      │ (game server,     │
                                      │  robot, API, etc.)│
                                      └──────────────────┘
```

**Credit usage**: 1 credit to kick off + 1 for final analysis. Periodic reviews
are optional. A 30-minute session typically costs 1–2 credits total.

### Set your Omni Key

`ToolRunner` reads your key from the `OMNI_KEY` environment variable:

```bash
export OMNI_KEY="olink_YOUR_KEY_HERE"
```

You can also override it per-runner by setting `omni_key` as a class attribute.

### Minimal example

```python
from omnilink.tool_runner import ToolRunner

class MyRunner(ToolRunner):
    agent_name = "my-agent"
    display_name = "My Task"

    def get_state(self):
        # Fetch state from your target system (HTTP, socket, file, etc.)
        return requests.get("http://localhost:8000/state").json()

    def execute_action(self, state):
        # Decide what to do and send the action
        action = "UP" if state["y"] < state["target_y"] else "DOWN"
        requests.post("http://localhost:8000/action", json={"action": action})

    def state_summary(self, state):
        # Concise text for the agent's memory
        return f"Position: ({state['x']}, {state['y']}), Target: ({state['target_x']}, {state['target_y']})"

    def is_game_over(self, state):
        return state.get("done", False)

if __name__ == "__main__":
    MyRunner().run()
```

That's it — `python my_runner.py` will:

1. Create/update the agent profile on OmniLink
2. Ask the cloud agent to call `make_move` (1 API credit)
3. Run your local loop: poll state → execute action → repeat
4. Persist state to agent memory every 60 seconds
5. Listen for pause/resume/stop commands from the OmniLink UI
6. Print a final summary and ask the agent for analysis (1 API credit)

### Real-world example (Breakout)

```python
from omnilink.tool_runner import ToolRunner
from breakout_api import get_state, send_action
from breakout_engine import decide_action, state_summary

class BreakoutRunner(ToolRunner):
    agent_name = "breakout-agent"
    display_name = "Breakout"
    tool_description = "Move paddle."

    def __init__(self):
        self._last_score = 0

    def get_state(self):
        return get_state()

    def execute_action(self, state):
        if state.get("game_state") == "PLAY":
            send_action(decide_action(state))

    def state_summary(self, state):
        return state_summary(state)

    def is_game_over(self, state):
        return state.get("game_state") == "GAMEOVER"

    def on_start(self):
        send_action("RESUME")

    def log_events(self, state):
        score = state.get("score", 0)
        if score != self._last_score:
            print(f"  Score: {score}  (+{score - self._last_score})")
            self._last_score = score

if __name__ == "__main__":
    BreakoutRunner().run()
```

---

## ToolRunner reference

`ToolRunner` is a base class in `omnilink.tool_runner`. Subclass it and
override the hooks below to build any local tool controller.

### Configuration (class attributes)

| Attribute | Default | Description |
|---|---|---|
| `agent_name` | `"tool-agent"` | Agent profile name on OmniLink. |
| `display_name` | `"Tool"` | Human-readable name (used in banners and logs). |
| `base_url` | `"https://www.omnilink-agents.com"` | OmniLink API base URL. |
| `omni_key` | `os.environ["OMNI_KEY"]` | Your Omni Key (reads from `OMNI_KEY` env var by default). |
| `engine` | `"g2-engine"` | AI engine to use (`g1-engine`, `g2-engine`, etc.). |
| `poll_interval` | `0.0` | Seconds between ticks (0 = as fast as possible). |
| `memory_every` | `60` | Save state to agent memory every N seconds. |
| `ask_every` | `2400` | Periodic agent review interval in seconds. |
| `tool_name` | `"make_move"` | Name of the tool the agent calls. |
| `tool_description` | `"Execute the next action."` | Tool description for the agent. |
| `commands` | `"stop_game, pause_game, resume_game"` | Available UI commands. |

### Required hooks (must override)

| Method | Signature | Description |
|---|---|---|
| `get_state()` | `→ dict` | Fetch current state from the target system. |
| `execute_action(state)` | `→ None` | Decide and send the next action. |
| `state_summary(state)` | `→ str` | Concise text summary for agent memory. |
| `is_game_over(state)` | `→ bool` | Return `True` when the task is finished. |

### Optional hooks

| Method | Default | Description |
|---|---|---|
| `on_start()` | No-op | Called after kickoff, before the main loop (e.g. send RESUME). |
| `log_events(state)` | No-op | Print noteworthy events each tick. |
| `game_over_message(state)` | `"GAME OVER"` | Text for the game-over banner. |
| `get_system_instruction()` | Auto-generated | Override for custom kickoff prompt. |
| `get_review_instruction()` | Auto-generated | Override for custom review prompt. |
| `get_profile_settings()` | Auto-generated | Override for custom agent profile. |

### Built-in behaviour

The `run()` method handles the full lifecycle automatically:

1. **Profile setup** — creates or updates the agent profile
2. **Tool-call kickoff** — one API call to trigger the tool
3. **Main loop** — calls `get_state()` → `is_game_over()` → `execute_action()` → `log_events()`
4. **Memory persistence** — saves `state_summary()` every `memory_every` seconds
5. **UI command polling** — checks memory for `stop_game` / `pause_game` / `resume_game`
6. **Periodic review** — asks the agent to review and decide continue/stop every `ask_every` seconds
7. **Final analysis** — saves final state and asks the agent for a summary

---

## Core concepts — command engine

The command engine is the second major system in OmniLink. While `ToolRunner`
handles cloud-orchestrated local tools, the command engine handles
natural-language command routing — useful for chatbots, smart-home controllers,
and interactive agents.

Before writing any code, it helps to understand the four building blocks:

| Concept | What it does |
|---|---|
| **`OmniLinkEngine`** | Matches incoming text against your templates, extracts variables, and calls the right handler. |
| **Templates** | Plain-English patterns that describe the commands your agent understands, e.g. `"turn on the [room] lights"`. |
| **Handlers** | Python functions you write. The engine calls them when a template matches. |
| **Bridges** | Optional transport adapters. A bridge connects the engine to a network — HTTP or MQTT — so external systems can send commands and receive replies. |
| **`AgentMessenger`** | An object injected into your handler by a bridge. It lets your handler send progress updates, ask the operator a question, and acknowledge receipt. |

You can use the engine alone (direct Python calls) or attach a bridge for
network connectivity. Handlers always look the same regardless of which bridge
is in use.

---

## Quick start — engine only

The simplest possible setup: create an engine, register a handler, call it.

```python
from omnilink import OmniLinkEngine

engine = OmniLinkEngine([
    "hello",
    "echo [message:any]",
])

def handle_hello(event):
    return {"message": "Hello, world!"}

def handle_echo(event):
    text = event["vars"]["message"].replace("_", " ")
    return {"echo": text}

engine.on_template("hello", handle_hello)
engine.on_template("echo [message:any]", handle_echo)

result = engine.handle("hello")
print(result["result"])          # {"message": "Hello, world!"}

result = engine.handle("echo good morning")
print(result["result"])          # {"echo": "good morning"}
```

`engine.handle()` always returns a dict that includes:

| Key | Meaning |
|---|---|
| `ok` | `True` if a handler ran without error, `False` otherwise. |
| `template` | The template that matched (or `None`). |
| `vars` | Dict of extracted variables. |
| `result` | Whatever your handler returned. |
| `errors` | List of type-conversion errors, if any. |

---

## Templates and types

### Defining templates

Templates are plain strings. Spaces and underscores are interchangeable — the
engine normalises both before matching.

```python
templates = [
    "status",                              # no variables
    "launch [vehicle]",                    # one variable (any word)
    "set speed to [speed:float]",          # typed variable
    "move [color] [piece] to [square]",    # multiple variables
    "say [message:any]",                   # greedy — matches multiple words
]
```

### Variable syntax

| Syntax | Matches |
|---|---|
| `[name]` | Any single word (letters, digits, underscores, hyphens). |
| `[name:int]` | Integer — converted automatically. |
| `[name:float]` | Float — converted automatically. |
| `[name:any]` | One or more words (greedy). |
| `[name:alpha]` | Letters only. |
| `[name:uuid]` | UUID string. |
| `[name:/regex/]` | Custom regular expression. |

### Registering custom types

```python
from omnilink import OmniLinkEngine, TypeRegistry

types = TypeRegistry()
types.register(
    "room",
    r"(?:living_room|kitchen|bedroom|office)",   # regex pattern
    lambda raw: raw.replace("_", " "),           # optional converter
)

engine = OmniLinkEngine(
    ["turn on the [room:room] lights"],
    types=types,
)
```

### Loading templates from a file

Keep templates in a text file (one per line, `#` for comments):

```
# smart_home/commands.txt
turn on the [room:room] lights
turn off the [room:room] lights
set thermostat to [temp:float] degrees
lock the [door:door]
```

```python
from omnilink import load_patterns_from_file

templates = load_patterns_from_file("smart_home/commands.txt")
engine = OmniLinkEngine(templates, types=types)
```

---

## Writing handlers

### The event dictionary

Every handler receives a single `event` dict:

```python
def my_handler(event: dict) -> dict:
    event["command"]    # raw input text, e.g. "launch falcon9"
    event["template"]   # matched template, e.g. "launch_[vehicle]"
    event["vars"]       # extracted vars, e.g. {"vehicle": "falcon9"}
    event["meta"]       # metadata passed by the caller or bridge
    event["messenger"]  # AgentMessenger (only when a bridge is running)
    event["timestamp"]  # Unix timestamp
```

### Registering handlers

```python
# Match a specific template by name
engine.on_template("launch [vehicle]", handle_launch)

# Match with a custom predicate
engine.on(lambda e: e["vars"].get("speed", 0) > 100, handle_high_speed)

# Fallback — runs when nothing else matches
engine.on(lambda e: e["template"] is None, handle_unknown)
```

### Middleware

Run a function before or after every command:

```python
def log_command(event):
    print(f"[{event['timestamp']}] {event['command']}")

engine.before(log_command)
```

### Returning results

Return any JSON-serialisable dict. The value lands in `result["result"]`.
Return `None` to indicate a no-op.

---

## Bidirectional messaging

When your engine is connected to a bridge, the `event["messenger"]` object lets
your handler communicate back to the operator in real time.

### Acknowledge receipt

```python
messenger = event["messenger"]
messenger.acknowledge("pending", message="Starting sequence...")
```

### Send progress updates

```python
from omnilink import AgentFeedback

messenger.send_feedback(AgentFeedback(
    message="Pressurizing tanks",
    kind="info",       # "info" | "success" | "warning" | "error"
    progress=0.4,      # 0.0 – 1.0
    ok=True,
))
```

### Ask the operator a question

Your handler **blocks** at `pending.wait()` until the operator replies. Other
commands queued behind it continue to be processed normally.

```python
from omnilink import AgentQuestion

question = AgentQuestion(
    prompt="Confirm liftoff?",
    choices=["yes", "no"],
    data={"vehicle": event["vars"]["vehicle"]},
)
pending = messenger.ask_question(question)

try:
    reply = pending.wait(timeout=30, cancel_on_timeout=True)
except TimeoutError:
    return {"status": "timed out"}

if reply.get("answer") == "yes":
    return {"status": "launched"}
return {"status": "aborted"}
```

### Full handler example

```python
from omnilink import AgentFeedback, AgentQuestion, OmniLinkEngine

engine = OmniLinkEngine(["launch [vehicle]"])

def handle_launch(event):
    messenger = event.get("messenger")
    vehicle = event["vars"]["vehicle"]

    if messenger:
        messenger.acknowledge("pending", message=f"Preparing {vehicle}")
        messenger.send_feedback(AgentFeedback("Running pre-flight checks", progress=0.2))

        reply = messenger.ask_question(
            AgentQuestion(f"Confirm launch of {vehicle}?", choices=["yes", "no"])
        ).wait(timeout=30, cancel_on_timeout=True)

        if reply.get("answer") != "yes":
            return {"status": "aborted"}

        messenger.send_feedback(AgentFeedback("Engines ignited", progress=0.8, kind="success"))

    return {"status": "launched", "vehicle": vehicle}

engine.on_template("launch [vehicle]", handle_launch)
```

---

## Connecting over HTTP

`OmniLinkHTTPBridge` turns your engine into a local HTTP server. Any client
that can make HTTP requests — a browser, `curl`, another Python script — can
send commands and receive responses.

### Start the server

```python
from omnilink import OmniLinkEngine, OmniLinkHTTPBridge

engine = OmniLinkEngine(["launch [vehicle]"])
engine.on_template("launch [vehicle]", handle_launch)  # your handler above

bridge = OmniLinkHTTPBridge(engine, host="0.0.0.0", port=8080)
bridge.loop_forever()   # blocks; use bridge.start() for background mode
```

### Sending a command

```bash
curl -s -X POST http://localhost:8080/command \
  -H "Content-Type: application/json" \
  -d '{"command": "launch falcon9"}'
```

Response:

```json
{"requestId": "b3d2…", "status": "accepted"}
```

The command is processed **asynchronously** in a background thread. Use the
`requestId` to follow its progress.

### Polling for feedback

```bash
curl http://localhost:8080/feedback/b3d2…
```

```json
{
  "requestId": "b3d2…",
  "command": "launch falcon9",
  "done": false,
  "acks":      [{"status": "pending", "message": "Preparing falcon9"}],
  "feedback":  [{"message": "Running pre-flight checks", "progress": 0.2}],
  "questions": [{"prompt": "Confirm launch of falcon9?", "choices": ["yes","no"], "correlationId": "c1a9…"}]
}
```

### Answering a question

```bash
curl -s -X POST http://localhost:8080/reply \
  -H "Content-Type: application/json" \
  -d '{"correlationId": "c1a9…", "answer": "yes"}'
```

The handler unblocks and finishes processing.

### Getting the final result

```bash
curl http://localhost:8080/result/b3d2…
```

```json
{"requestId": "b3d2…", "done": true, "ok": true, "result": {"status": "launched", "vehicle": "falcon9"}}
```

### Other endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/context` | Engine metrics and recent command history. |
| `GET` | `/state` | Alias for `/context`. |

### Configuration

| Environment variable | Default | Description |
|---|---|---|
| `HTTP_BRIDGE_HOST` | `0.0.0.0` | Bind address. |
| `HTTP_BRIDGE_PORT` | `8080` | Bind port. |

---

## Connecting over MQTT

`OmniLinkMQTTBridge` connects the engine to an MQTT broker. Commands arrive as
published messages; feedback and questions are published back to sub-topics
automatically.

### Start the bridge

```python
from omnilink import OmniLinkEngine, OmniLinkMQTTBridge

engine = OmniLinkEngine(["launch [vehicle]"])
engine.on_template("launch [vehicle]", handle_launch)

bridge = OmniLinkMQTTBridge(engine, host="localhost", transport="websockets")
bridge.loop_forever()
```

### Sending a command

Publish to the command topic (default `olink/commands`):

```json
{"command": "launch falcon9"}
```

You can override the feedback topic per message:

```json
{"command": "launch falcon9", "reply_to": "myapp/feedback"}
```

### Topic layout

| Topic | Direction | Content |
|---|---|---|
| `olink/commands` | → engine | Command payloads. |
| `olink/commands_feedback` | ← engine | Legacy simplified responses. |
| `<feedback>/agent_ack` | ← engine | Acknowledgement from `messenger.acknowledge()`. |
| `<feedback>/agent_feedback` | ← engine | Progress updates from `messenger.send_feedback()`. |
| `<feedback>/agent_questions` | ← engine | Questions from `messenger.ask_question()`. |
| `<feedback>/agent_replies` | → engine | Operator answers (subscribed automatically). |
| `olink/state` | ← engine | State snapshots. |
| `olink/context` | ← engine | Context updates. |

### Answering a question over MQTT

Publish to `<feedback>/agent_replies`:

```json
{"correlationId": "c1a9…", "answer": "yes"}
```

### Publishing a state snapshot

```python
bridge.publish_state_snapshot(history_limit=10)
```

### Configuration

| Environment variable | Default | Description |
|---|---|---|
| `MQTT_HOST` | `localhost` | Broker hostname. |
| `MQTT_PORT` | `9001` (WS) / `1883` (TCP) | Broker port. |
| `MQTT_TRANSPORT` | `websockets` | `websockets` or `tcp`. |
| `MQTT_USERNAME` | — | Optional username. |
| `MQTT_PASSWORD` | — | Optional password. |
| `MQTT_COMMAND_TOPIC` | `olink/commands` | Inbound commands. |
| `MQTT_FEEDBACK_TOPIC` | `olink/commands_feedback` | Outbound feedback base. |
| `MQTT_STATE_TOPIC` | `olink/state` | State snapshot topic. |
| `MQTT_CONTEXT_TOPIC` | `olink/context` | Context update topic. |
| `MQTT_AUTOSTART` | `1` | Auto-start Mosquitto on localhost. |

---

## Pluggable use cases

A **use case** is a self-contained bundle of types, templates, and handler
registrations. The use case loader discovers the active use case at startup
from the `OmniLink_USE_CASE` environment variable, which keeps the transport
bridges completely generic.

```python
# my_app/use_case.py
from omnilink import OmniLinkEngine, TypeRegistry, UseCaseConfig

def build_use_case() -> UseCaseConfig:
    types = TypeRegistry()
    types.register("zone", r"(?:zone_[A-Z]|all)")

    templates = ["arm [zone:zone]", "disarm all", "status"]

    def configure(engine: OmniLinkEngine) -> None:
        engine.on_template("arm [zone:zone]", handle_arm)
        engine.on_template("disarm all",      handle_disarm)
        engine.on_template("status",          handle_status)

    return UseCaseConfig(types=types, templates=templates, configure=configure)
```

```python
from omnilink import OmniLinkHTTPBridge
from omnilink.use_case_loader import load_use_case

use_case = load_use_case("my_app.use_case")
engine = OmniLinkEngine(use_case.templates, types=use_case.types)
use_case.configure(engine)

bridge = OmniLinkHTTPBridge(engine, host="0.0.0.0", port=8080)
bridge.loop_forever()
```

The default use case (when the variable is unset) is
`omnilink.examples.robot.use_case`.

---

## REST API client

`OmniLinkClient` is a full Python client for a hosted OmniLink server
(e.g. `https://www.omnilink-agents.com`). It covers chat, memory,
speech-to-text, text-to-speech, and translation.

```python
from omnilink.client import OmniLinkClient

client = OmniLinkClient(omni_key="omk_...")

# Chat
reply = client.chat("What is 2 + 2?", agent_name="math-tutor")
print(reply["text"])

# Save conversation memory
client.set_memory("math-tutor", [
    {"role": "user",  "parts": [{"text": "Hello"}]},
    {"role": "model", "parts": [{"text": "Hi!"}]},
])

# Speech-to-text
with open("audio.webm", "rb") as f:
    result = client.transcribe(f.read(), mime_type="audio/webm")
print(result["text"])

# Text-to-speech
audio = client.synthesize_to_bytes("Hello from OmniLink!")
with open("out.mp3", "wb") as f:
    f.write(audio)

# Translation
result = client.translate("Bonjour le monde", target_language="English")
print(result["translation"])
```

`OmniLinkClient` is independent of `OmniLinkEngine` — use it to communicate
with a remote OmniLink deployment from any Python script.

---

## Environment variable reference

### HTTP bridge

| Variable | Default | Description |
|---|---|---|
| `HTTP_BRIDGE_HOST` | `0.0.0.0` | Bind address. |
| `HTTP_BRIDGE_PORT` | `8080` | Bind port. |

### MQTT bridge

| Variable | Default | Description |
|---|---|---|
| `MQTT_HOST` | `localhost` | Broker hostname. |
| `MQTT_PORT` | `9001` / `1883` | Broker port. |
| `MQTT_TRANSPORT` | `websockets` | `websockets` or `tcp`. |
| `MQTT_USERNAME` | — | Optional username. |
| `MQTT_PASSWORD` | — | Optional password. |
| `MQTT_CLIENT_ID` | — | Optional client identifier. |
| `MQTT_COMMAND_TOPIC` | `olink/commands` | Inbound command topic. |
| `MQTT_FEEDBACK_TOPIC` | `olink/commands_feedback` | Outbound feedback base topic. |
| `MQTT_RESPONSE_TOPIC` | — | Legacy alias for `MQTT_FEEDBACK_TOPIC`. |
| `MQTT_STATE_TOPIC` | `olink/state` | State snapshot topic. |
| `MQTT_CONTEXT_TOPIC` | `olink/context` | Context update topic. |
| `MQTT_INLINE_CODE_TOPIC` | `olink/inline_code` | Inline code execution topic. |
| `MQTT_QOS_SUB` | `0` | QoS level for subscriptions. |
| `MQTT_QOS_PUB` | `0` | QoS level for publishes. |
| `MQTT_KEEPALIVE` | `60` | MQTT keepalive in seconds. |
| `MQTT_ENCODING` | `utf-8` | Message encoding. |
| `MQTT_AUTOSTART` | `1` | Auto-start local Mosquitto broker. |

### Use case discovery

| Variable | Default | Description |
|---|---|---|
| `OmniLink_USE_CASE` | `omnilink.examples.robot.use_case` | Dotted module path that exports `build_use_case()`. |

---

## Running the built-in examples

All examples live under `src/omnilink/examples/`. Run any of them directly:

```bash
# Minimal engine demo — no bridge needed
python -m omnilink.examples.hello_world

# Full REST API client walkthrough (requires OMNI_KEY)
OMNI_KEY=omk_... python -m omnilink.examples.client_demo
```

To start an HTTP or MQTT bridge, instantiate the bridge class directly in your
script (see the door controller example under `examples/door_controller/`).

---

## Running the tests

```bash
pytest omnilink-lib/tests
```
