Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Ollama integration ([#20](https://github.com/ARPAHLS/aura/issues/20))** — `integrations/ollama/llama_loop.py` stdlib HTTP body loop; README; mocked HTTP test; docs index distinguishes Ollama-only vs Ollama+Skillware paths.

### Changed

- **Verified operator identity ([#55](https://github.com/ARPAHLS/aura/issues/55))** — optional identity adapters (manual, mock, OIDC, Auth0); `identity.bound` spine event; `ids.operator` on all event trailers; export redaction; `aura identity show`; profile `types` with `role: identity`.
Expand Down
3 changes: 2 additions & 1 deletion docs/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Attach AURA to your stack — models, tool runtimes, frameworks, sandboxes.
| **Overview** | this page | Start here to find your stack |
| **Skillware** | [`integrations/skillware/`](../../integrations/skillware/) | Reference ToolHost adapter; `[skillware]` extra |
| **Operator identity** | [`integrations/identity/`](../../integrations/identity/) | Optional OIDC/Auth0/manual/mock adapters; `[identity]` extra |
| **Ollama (local)** | [`integrations/skillware/ollama_skill_loop.py`](../../integrations/skillware/ollama_skill_loop.py) | Dev default: `llama3.2:1b` via `.env` |
| **Ollama (local)** | [`integrations/ollama/`](../../integrations/ollama/) | Dev default: `llama3.2:1b` via `.env`; stdlib HTTP |
| **Ollama + Skillware** | [`integrations/skillware/ollama_skill_loop.py`](../../integrations/skillware/ollama_skill_loop.py) | Local model body with Skillware egress |
| **OpenAI (ChatGPT)** | [`integrations/openai/`](../../integrations/openai/) | Body loop + Skillware egress; `[openai]` extra |
| **Anthropic (Claude)** | [`integrations/anthropic/`](../../integrations/anthropic/) | Body loop + Skillware egress; `[anthropic]` extra |
| **Google Gemini** | [`integrations/google/`](../../integrations/google/) | Body loop + Skillware egress; `[google]` extra |
Expand Down
38 changes: 38 additions & 0 deletions integrations/ollama/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Ollama + AURA

Run a local Ollama model as the **body** inside an AURA session.

This example uses only Python stdlib HTTP and the shared stdlib `.env` loader in
`integrations/_shared/env.py`; it does not require `python-dotenv` or the Ollama
Python package.

## Setup

```powershell
ollama pull llama3.2:1b
copy .env.example .env
```

Set in `.env`:

```
OLLAMA_BASE_URL=http://127.0.0.1:11434
OLLAMA_MODEL=llama3.2:1b
```

`AURA_HOME` is optional and controls where AURA writes local session data.

Cloud body loops use the same `.env` pattern:

- `OPENAI_API_KEY`, `OPENAI_MODEL` in [`../openai/`](../openai/)
- `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` in [`../anthropic/`](../anthropic/)
- `GOOGLE_API_KEY`, `GEMINI_MODEL` in [`../google/`](../google/)

## Run

```powershell
python integrations/ollama/llama_loop.py
```

The script opens `with ag.session()`, emits `turn.start`, calls Ollama
`/api/chat`, emits `model.call`, then emits `turn.end`.
81 changes: 81 additions & 0 deletions integrations/ollama/llama_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Minimal Ollama body loop under an AURA session."""

from __future__ import annotations

import json
import os
import sys
import urllib.request
from pathlib import Path
from typing import Any

_REPO = Path(__file__).resolve().parents[2]
if str(_REPO) not in sys.path:
sys.path.insert(0, str(_REPO))

from integrations._shared.env import load_dotenv # noqa: E402

load_dotenv(_REPO)

from aura import agent, configure # noqa: E402


def _ollama_chat(
base_url: str,
model: str,
messages: list[dict[str, str]],
*,
timeout: int = 30,
) -> str:
url = f"{base_url.rstrip('/')}/api/chat"
body = json.dumps({"model": model, "messages": messages, "stream": False}).encode("utf-8")
request = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
payload: dict[str, Any] = json.loads(response.read().decode("utf-8"))
return str(payload.get("message", {}).get("content", ""))


def run_loop(prompt: str) -> dict[str, Any]:
base_url = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434")
model = os.environ.get("OLLAMA_MODEL", "llama3.2:1b")
configure()

ag = agent(
"ollama-llama-loop",
purpose="Ollama body loop under AURA audit",
)
with ag.session(mode="script") as run:
run.emit("turn.start", {"input": prompt, "provider": "ollama", "model": model})
output = _ollama_chat(
base_url,
model,
[
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": prompt},
],
)
run.emit("model.call", {"provider": "ollama", "model": model, "output": output[:500]})
run.emit("turn.end", {"output": output})

return {
"session_id": run.session_id,
"provider": "ollama",
"model": model,
"output": output,
"exports": run.exports,
}


def main() -> None:
prompt = "Say hello from AURA."
print(json.dumps(run_loop(prompt), indent=2, default=str))


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions tests/test_ollama_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Ollama loop tests with mocked HTTP."""

from __future__ import annotations

import json
from pathlib import Path

from integrations.ollama import llama_loop


class _MockResponse:
status = 200

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
return False

def read(self) -> bytes:
return json.dumps({"message": {"content": "mocked llama reply"}}).encode("utf-8")


def test_ollama_loop_emits_model_call_without_network(monkeypatch, aura_home):
seen = {}

def fake_urlopen(request, timeout):
seen["url"] = request.full_url
seen["timeout"] = timeout
seen["body"] = json.loads(request.data.decode("utf-8"))
return _MockResponse()

monkeypatch.setenv("OLLAMA_BASE_URL", "http://ollama.test")
monkeypatch.setenv("OLLAMA_MODEL", "llama3.2:1b")
monkeypatch.setattr(llama_loop.urllib.request, "urlopen", fake_urlopen)

result = llama_loop.run_loop("hello")

assert result["output"] == "mocked llama reply"
assert result["model"] == "llama3.2:1b"
assert seen["url"] == "http://ollama.test/api/chat"
assert seen["body"]["stream"] is False
assert seen["body"]["model"] == "llama3.2:1b"
assert [m["role"] for m in seen["body"]["messages"]] == ["system", "user"]

jsonl = Path(result["exports"]["jsonl"])
kinds = [json.loads(line)["kind"] for line in jsonl.read_text(encoding="utf-8").splitlines()]
assert "model.call" in kinds
assert kinds.index("turn.start") < kinds.index("model.call") < kinds.index("turn.end")
Loading