Metadata-Version: 2.4
Name: omnilink
Version: 0.3.0
Summary: OmniLink natural language engine and integration bridges
Author-email: OmniLink <support@omnilink-agents.com>
License: MIT
Project-URL: Homepage, https://www.omnilink-agents.com
Project-URL: Documentation, https://www.omnilink-agents.com/documentation
Project-URL: Repository, https://github.com/omnilink/omnilink
Keywords: omnilink,ai,agents,nlp,assistant,chat,tts,stt,iot
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31
Provides-Extra: bridges
Requires-Dist: websocket-client>=1.6.0; extra == "bridges"

# OmniLink Python Library

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

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

---

## Table of Contents

1. [Installation](#installation)
2. [Get your Omni Key](#get-your-omni-key)
3. [Quick start — ToolRunner](#quick-start--toolrunner)
4. [ToolRunner reference](#toolrunner-reference)
5. [Built-in benchmark examples](#built-in-benchmark-examples)
6. [Core concepts — command engine](#core-concepts--command-engine)
7. [Quick start — engine only](#quick-start--engine-only)
8. [Templates and types](#templates-and-types)
9. [Writing handlers](#writing-handlers)
10. [Bidirectional messaging](#bidirectional-messaging)
11. [Connecting over HTTP](#connecting-over-http)
12. [REST API client](#rest-api-client)
13. [Environment variable reference](#environment-variable-reference)
14. [Running the examples](#running-the-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 (`requests`)
are installed automatically. The Chess example additionally requires the
`chess` package (`pip install chess`).

---

## Get Your Omni Key

Before using the library you need an Omni Key (`olink_...`). Generate one with a single click:

**[Get your Omni Key](https://www.omnilink-agents.com/omnilink-api)**

Sign in (or create a free account) and click **Generate API key**. Copy the key — you will need it for every API call.

---

## Quick start — ToolRunner

A **ToolRunner** 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, SERVER_URL
from .breakout_engine import decide_action, state_summary

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

    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. |
| `game_server_url` | `None` | URL of the local game/system server (for pause/resume 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

---

## Built-in benchmark examples

The library ships with eight game benchmark examples, each following the same
three-file pattern: `play_*.py` (ToolRunner subclass), `*_api.py` (HTTP client
to the game server), and `*_engine.py` (local AI logic).

| Example | Agent Name | AI Strategy | Key Algorithms |
|---|---|---|---|
| **Pac-Man** | `pacman-agent` | Survival-first BFS | BFS pathfinding, ghost avoidance, dead-end detection, junction preference |
| **Chess** | `chess-agent` | Minimax search | Alpha-beta pruning, piece-square tables, move ordering |
| **Tetris** | `tetris-agent` | Optimal placement | Pierre Dellacherie evaluation, macro action batching |
| **Breakout** | `breakout-agent` | Ball prediction | Trajectory simulation with brick collision, paddle deflection physics |
| **Pong** | `pong-agent` | Full simulation | Ball trajectory with opponent AI modelling, integer-exact positioning |
| **Space Invaders** | `space-invaders-agent` | Lead-shot targeting | Stateful direction tracking, wall-bounce reflection, threat scoring |
| **Montezuma** | `montezuma-agent` | Safe pathfinding | BFS with enemy avoidance radius, fallback strategies |
| **Go** | `go-agent` | Heuristic evaluation | Capture priority, atari rescue, influence scoring, territory estimation |

Each example integrates with the OmniLink platform for profile management,
memory persistence, and periodic agent reviews. Run any example with:

```bash
export OMNI_KEY="olink_YOUR_KEY_HERE"
python -m omnilink.examples.pacman.play_pacman
python -m omnilink.examples.chess.play_chess
python -m omnilink.examples.tetris.play_tetris
# ... etc.
```

Each game requires its corresponding game server running locally. See the
`omnilink-benchmarks/` directory for the game servers.

---

## 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, 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 via HTTP 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):

```
# 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("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"}
```

---

## 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)

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
{"status": "ok", "command": "launch falcon9"}
```

The command is processed **asynchronously** in a background thread.

### Polling for feedback

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

Returns the latest feedback from the most recent command.

### Endpoint layout

| Method | Path | Description |
|---|---|---|
| `POST` | `/command` | Submit a command (JSON body with `command` field). |
| `GET` | `/feedback` | Latest feedback from the last processed command. |
| `GET` | `/context` | Latest context data / state snapshot. |
| `POST` | `/inline-code` | Submit inline code snippets. |

### Publishing state snapshots

```python
bridge.publish_context("System is ready")
bridge.publish_state_snapshot(history_limit=10)
```

### Configuration

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

---

## REST API client

`OmniLinkClient` is a full Python client for the OmniLink platform.
It covers chat, agent profiles, memory, speech-to-text, text-to-speech, and
translation.

```python
from omnilink.client import OmniLinkClient

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

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

# Agent profiles
profile = client.create_profile("my-agent", settings={
    "agentName": "my-agent",
    "mainTask": "You are a helpful assistant.",
    "allowToolUse": True,
    "availableTools": "make_move",
    "availableToolDetails": [{"name": "make_move", "description": "Run the AI."}],
    "availableCommands": "stop_game, pause_game, resume_game",
})
profiles = client.list_profiles()
client.update_profile(profile["id"], name="my-agent", settings={...})
client.delete_profile(profile["id"])

# Save and retrieve conversation memory
client.set_memory("my-agent", [
    {"role": "user",  "parts": [{"text": "Hello"}]},
    {"role": "model", "parts": [{"text": "Hi!"}]},
])
memory = client.get_memory("my-agent")
client.clear_memory("my-agent")

# 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["translatedText"])
```

`OmniLinkClient` is independent of `OmniLinkEngine` — use it to communicate
with a remote OmniLink deployment from any Python script. There is also a
lightweight `OmniLinkChatClient` for chat-only use cases.

---

## Environment variable reference

| Variable | Default | Description |
|---|---|---|
| `OMNI_KEY` | — | Your Omni Key. Used by `ToolRunner` and as a fallback for `OmniLinkClient`. |
| `HTTP_BRIDGE_HOST` | `0.0.0.0` | HTTP bridge bind address. |
| `HTTP_BRIDGE_PORT` | `8080` | HTTP bridge bind port. |

---

## Running the examples

All examples live under `src/omnilink/examples/`. The library includes:

### Standalone scripts

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

# Chat client quickstart (requires OMNI_KEY)
OMNI_KEY=olink_... python -m omnilink.examples.chat_quickstart

# Full REST API client walkthrough
OMNI_KEY=olink_... python -m omnilink.examples.client_demo

# Tool-calling with arithmetic operations
OMNI_KEY=olink_... python -m omnilink.examples.arithmetic_tools
```

### ToolRunner benchmark games

Each game requires its corresponding game server from `omnilink-benchmarks/`:

```bash
# Start the game server first, then run the agent:
OMNI_KEY=olink_... python -m omnilink.examples.pacman.play_pacman
OMNI_KEY=olink_... python -m omnilink.examples.chess.play_chess
OMNI_KEY=olink_... python -m omnilink.examples.tetris.play_tetris
OMNI_KEY=olink_... python -m omnilink.examples.breakout.play_breakout
OMNI_KEY=olink_... python -m omnilink.examples.pong.play_pong
OMNI_KEY=olink_... python -m omnilink.examples.space_invaders.play_space_invaders
OMNI_KEY=olink_... python -m omnilink.examples.montezuma.play_montezuma
OMNI_KEY=olink_... python -m omnilink.examples.go.play_go
```

---

## Running the tests

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