Skip to content
Open
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
7 changes: 7 additions & 0 deletions services/domyn_agent_example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DOMYN_API_KEY=your_api_key_here
DOMYN_CHANNEL_ID=your_channel_id_here
DOMYN_SPACE_ID=your_space_id_here
DOMYN_BASE_URL=analytics-az.crystal.io
VLLM_API_KEY=your_vllm_api_key_here
VLLM_BASE_URL=https://gateway-dev.llm.crystal.ai/v1
VLLM_MODEL=Qwen/Qwen3-32B
20 changes: 20 additions & 0 deletions services/domyn_agent_example/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3.13-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Install domyn-agents from the bundled wheel (no private index required)
COPY wheels/ wheels/
RUN pip install --no-cache-dir wheels/domyn_agents-*.whl

# Copy the agent
COPY agent_expose.py .

# Connect to the Domyn platform via WebSocket relay
# All required values come from environment variables (see .env.example)
CMD ["sh", "-c", \
"domyn expose agent_expose:agent \
--channel-id $DOMYN_CHANNEL_ID \
--space-id $DOMYN_SPACE_ID \
--base-url $DOMYN_BASE_URL"]
218 changes: 218 additions & 0 deletions services/domyn_agent_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Domyn Platform — Native domyn-agents ReAct Agent Blueprint

A ready-to-run example of a native domyn-agents ReAct agent connected to the Domyn platform as a subagent. The agent uses an LLM (via a vLLM-compatible endpoint) and a set of built-in tools to answer tasks sent by the platform orchestrator.

---

## What's included

```
domyn_agent_example/
├── agent_expose.py # The agent definition — modify tools and LLM config here
├── test_local.py # Run the agent locally without a WebSocket connection
├── Dockerfile # Container image definition
├── docker-compose.yml # One-command local Docker run
├── requirements.txt # Python dependencies (excl. domyn-agents wheel)
├── .env.example # Required environment variables
└── wheels/ # Place the domyn-agents .whl file here
```

---

## Prerequisites

- Python 3.11+ (for local/laptop runs)
- Docker (for containerised runs and VM deployment)
- The `domyn-agents` wheel file — place it in `wheels/`

---

## Step 0 — Prepare the wheel

Obtain the `domyn_agents-*.whl` file and place it inside the `wheels/` directory before running anything:

```bash
ls wheels/
```

---

## Step 1 — Configure environment variables

```bash
cp .env.example .env
# Edit .env and fill in your values
```

| Variable | Description |
|---|---|
| `DOMYN_API_KEY` | API key from the Domyn platform |
| `DOMYN_CHANNEL_ID` | WebSocket channel ID assigned to this subagent |
| `DOMYN_SPACE_ID` | Your space ID on the platform |
| `DOMYN_BASE_URL` | Platform base URL |
| `VLLM_API_KEY` | API key for the vLLM/LLM gateway |
| `VLLM_BASE_URL` | Base URL of the LLM gateway (OpenAI-compatible, full path to `/v1/chat/completions`) |
| `VLLM_MODEL` | Model name to use (e.g. `Qwen/Qwen3-32B`) |

---

## Running locally (laptop)

Install dependencies:

```bash
pip install wheels/domyn_agents-*.whl
```

Test the agent without any platform connection:

```bash
python test_local.py
# or with a custom task:
python test_local.py "Add 5 and 7"
```

Connect to the platform:

```bash
source .env
domyn expose agent_expose:agent \
--channel-id $DOMYN_CHANNEL_ID \
--space-id $DOMYN_SPACE_ID \
--base-url $DOMYN_BASE_URL
```

`domyn expose` accepts a `module:symbol` argument pointing to a domyn `Agent` instance. `agent_expose` is the Python module (i.e. `agent_expose.py`) and `agent` is the `Agent` instance exported from it.

The process stays running and reconnects automatically on network drops.

---

## Running with Docker (laptop or VM)

Build the image:

```bash
docker build -t domyn-agent-blueprint .
```

Run (reads credentials from `.env`):

```bash
docker run --env-file .env domyn-agent-blueprint
```

Or with Docker Compose (one command):

```bash
docker compose up
```

---

## Deploying to a VM

1. Copy the blueprint directory to your VM:

```bash
scp -r services/domyn_agent_example/ user@your-vm:/opt/domyn-agent/
```

2. SSH into the VM and build:

```bash
ssh user@your-vm
cd /opt/domyn-agent
cp .env.example .env && nano .env # fill in credentials
docker build -t domyn-agent-blueprint .
```

3. Run as a systemd service for automatic restart on reboot:

```ini
# /etc/systemd/system/domyn-agent.service
[Unit]
Description=Domyn Native Agent Subagent
After=docker.service
Requires=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker run --rm --env-file /opt/domyn-agent/.env \
--name domyn-agent domyn-agent-blueprint
ExecStop=/usr/bin/docker stop domyn-agent

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now domyn-agent
sudo journalctl -fu domyn-agent # tail logs
```

---

## Agent overview

The agent is a domyn-agents ReAct agent backed by an OpenAI-compatible LLM. It receives a task from the platform orchestrator and reasons over a set of tools to produce a response.

### Built-in tools

| Tool | Description |
|---|---|
| `add_numbers` | Add two numbers |
| `multiply_numbers` | Multiply two numbers |
| `get_current_time` | Return the current UTC time |
| `reverse_string` | Reverse a string |
| `count_words` | Count words in a string |

### Adding tools

Open `agent_expose.py` and add a tool to the agent:

```python
from domyn_agents.core.decorators import tool

@tool(name="my_tool", description="Description shown to the LLM.")
def my_tool(x: str) -> str:
return x.upper()

agent = Agent(
...
tools=[..., my_tool],
)
```

### Changing the LLM

Edit `_get_llm()` in `agent_expose.py` or set the env vars `VLLM_MODEL`, `VLLM_BASE_URL`, `VLLM_API_KEY`.

The `OpenAIProvider` accepts any OpenAI-compatible endpoint (vLLM, Together, Groq, etc.).

---

## How it works

```
Platform orchestrator
│ AGENT_START (via WebSocket relay)
domyn expose agent_expose:agent
│ Receives AGENT_START, extracts task text
│ Runs domyn Runner with the Agent
Agent ReAct loop
├── LLM call (VLLM_MODEL via VLLM_BASE_URL)
├── Tool execution (local)
└── RESPONSE → streamed back to platform
```

The `domyn expose` command auto-detects that `agent_expose:agent` is a domyn `Agent` instance and uses the `DomynAgentRuntime` — no input mapper or LangChain callbacks required.

Multi-turn conversations are supported: the agent maintains conversation history per `conversation_id` across calls.
130 changes: 130 additions & 0 deletions services/domyn_agent_example/agent_expose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import logging
import os
from datetime import UTC, datetime

from domyn_agents.agents.agent import Agent
from domyn_agents.core.decorators import tool
from domyn_agents.llm.openai import OpenAIProvider
from domyn_agents.logger import set_logger
from domyn_agents.planner_strategy.tool_use import ToolUsePlannerStrategy
from domyn_agents.tools.delegate_tool import DelegateTool

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
set_logger(logging.getLogger("domyn_agents"))

# ---------------------------------------------------------------------------
# LLM
# ---------------------------------------------------------------------------


def _get_llm() -> OpenAIProvider:
api_key = os.getenv("VLLM_API_KEY") or os.getenv("VLLM_API_KEY_DEFAULT", "")
base_url = os.getenv("VLLM_BASE_URL", "https://gateway-dev.llm.crystal.ai/v1")
# OpenAIProvider needs the full completions endpoint; accept base-URL style too.
if not base_url.endswith("/chat/completions"):
base_url = base_url.rstrip("/") + "/chat/completions"
return OpenAIProvider(
model_name=os.getenv("VLLM_MODEL", "Qwen/Qwen3-32B"),
url=base_url,
api_key=api_key,
generation_params={
"temperature": 0.7,
"max_completion_tokens": 4000,
},
)


def _get_planner_with_stop() -> ToolUsePlannerStrategy:
return ToolUsePlannerStrategy(use_stop=True)


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@tool(name="add_numbers", description="Add two numbers together.")
def add_numbers(a: float, b: float) -> float:
return a + b


@tool(name="multiply_numbers", description="Multiply two numbers together.")
def multiply_numbers(a: float, b: float) -> float:
return a * b


@tool(name="get_current_time", description="Return the current UTC time as an ISO 8601 string.")
def get_current_time() -> str:
return datetime.now(UTC).isoformat()


@tool(name="reverse_string", description="Reverse a string.")
def reverse_string(text: str) -> str:
return text[::-1]


@tool(name="count_words", description="Count the number of words in a string.")
def count_words(text: str) -> int:
return len(text.split())


# ---------------------------------------------------------------------------
# Platform delegate tools
# Schema (parameters) is auto-fetched from the platform at expose-time by
# DomynAgentRuntime.initialize(); only the tool name is required here.
# ---------------------------------------------------------------------------

web_search = DelegateTool(tool_name="web_search")

# ---------------------------------------------------------------------------
# Agent
# ---------------------------------------------------------------------------

math_agent = Agent(
name="MathAgent",
description="Specialist agent for arithmetic operations (addition, multiplication).",
instruction=(
"You are a math specialist. Use the available arithmetic tools to "
"compute the requested result and return it concisely."
),
llm_provider=_get_llm(),
tools=[add_numbers, multiply_numbers],
planner=_get_planner_with_stop(),
)

string_agent = Agent(
name="StringAgent",
description="Specialist agent for string operations (reverse, word count).",
instruction=(
"You are a string-processing specialist. Use the available tools to "
"transform or analyze the input text and return the result concisely."
),
llm_provider=_get_llm(),
tools=[reverse_string, count_words],
planner=_get_planner_with_stop(),
)

agent = Agent(
name="DomynAgent",
planner=ToolUsePlannerStrategy(),
description=(
"Orchestrator agent. Delegates arithmetic to MathAgent and string "
"operations to StringAgent; answers time-related questions and "
"web searches directly."
),
instruction=(
"You are an orchestrator. For arithmetic questions delegate to "
"MathAgent. For string operations delegate to StringAgent. For "
"time-related questions use the get_current_time tool. "
"For questions that require current or external information use "
"the web_search tool. "
"If the user updates tool parameters mid-conversation, continue "
"with the new values."
),
llm_provider=_get_llm(),
tools=[get_current_time, web_search],
sub_agents=[math_agent, string_agent],
)
5 changes: 5 additions & 0 deletions services/domyn_agent_example/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
domyn-agent:
build: .
env_file: .env
restart: unless-stopped
3 changes: 3 additions & 0 deletions services/domyn_agent_example/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Install domyn-agents from the bundled wheel in ./wheels/
# Run: pip install wheels/domyn_agents-*.whl
# No additional dependencies required — domyn-agents is self-contained.
Loading