Skip to main content

Your First Agent

Let's get a real AI chatbot running with AgentVisor™. You'll have a working conversational agent in under five minutes.

What You'll Learn

  • Creating a project from a template and running it with agentvisor serve
  • Interacting with an agent via the HTTP API
  • How agent code, policies, and services fit together

Prerequisites

  • Docker installed and running
  • AgentVisor CLI installed (see Getting Started)
note

This tutorial creates a LangGraph agent. LangGraph is the recommended framework for stateful agents—see Supported Frameworks for all built-in providers.

Create the Project

agentvisor template create langgraph/chatbot-agent
cd chatbot-agent

This creates a ready-to-run project:

chatbot-agent/
├── agent.py # LangGraph agent code
├── langgraph.json # Graph configuration
├── requirements.txt # Python dependencies
├── compose.yml # Development services (Temporal + Ollama)
├── agentvisor.yaml # Runtime config (SSRF allowlist for the loopback Ollama endpoint)
├── mav-agent-config.yaml # Agent-level config (A2A skill metadata)
├── policies/
│ ├── domain.yml # Access control policies
│ └── test.yml # Policy test cases
├── .dockerignore # Files excluded from container builds
└── README.md # Template documentation

See Project Structure for details on what each file does.

Start Services and Run

# Start Temporal (workflow orchestration) and Ollama (local LLM)
docker compose up -d

# Run the agent
agentvisor serve .

The agentvisor serve command:

  1. Downloads a guest container image (first run only)
  2. Mounts your agent code into a sandbox
  3. Installs Python dependencies
  4. Starts the HTTP API on port 8090
Docker Sandbox Mode

agentvisor serve runs in Docker sandbox mode — production-grade containment that works on any platform (Linux, macOS, Windows). agentvisor build produces images using gVisor by default, adding syscall-level interception for slightly stronger defense-in-depth on Linux hosts. See Sandbox Modes and Deploying Your Agent for the full picture.

Once running, open http://localhost:8090/swagger-ui/ in your browser to explore the API interactively.

Test It

In a separate terminal:

# Create a conversation thread
THREAD=$(curl -sX POST http://localhost:8090/threads | jq -r '.thread_id')
echo "Thread: $THREAD"

# Send a message
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=60s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Hello! What can you help me with?"}]}}' \
| jq '.output.messages[-1].content'

You should see a conversational response from the chatbot.

Try a follow-up message — the agent remembers context from previous messages in the same thread:

curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=60s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Tell me more about that."}]}}' \
| jq '.output.messages[-1].content'

Key endpoints:

  • GET /agents — List available agents
  • POST /threads — Create conversation thread
  • POST /threads/{id}/runs?wait=30s — Execute a run
  • GET /threads/{id}/state — Get thread state

Understanding the Code

agent.py

The agent is a LangGraph state graph that calls a local LLM via Ollama.

Code Simplified for Learning

The code snippets below are simplified versions focused on key concepts. The actual agent.py source includes additional error handling, message type conversion, and uses ChatState rather than AgentState. See your project's agent.py (scaffolded above) for production patterns.

class AgentState(TypedDict):
"""Conversation state — messages auto-accumulate via add_messages."""
messages: Annotated[list, add_messages]

The add_messages annotation is a LangGraph feature that automatically appends new messages to the list rather than replacing it. This is how conversation history is maintained across runs.

def chat(state: AgentState) -> dict:
"""Call the LLM with the full conversation history."""
llm = ChatOllama(model=model, base_url=ollama_url)
response = llm.invoke(state["messages"])
return {"messages": [response]}

The chat function sends the full conversation history to Ollama and returns the response. Because it uses OLLAMA_BASE_URL (defaulting to http://localhost:11434), the HTTP request goes through AgentVisor's policy-enforced proxy — no direct network access. The template's agentvisor.yaml allowlists this loopback destination for the proxy's SSRF guard, which otherwise blocks loopback by default.

graph = builder.compile(checkpointer=AgentVisorCheckpointer())

AgentVisorCheckpointer implements LangGraph's checkpointer interface but persists state to the Temporal workflow, enabling state survival across restarts and multi-turn conversations.

langgraph.json

Declares available graphs and their entry points:

{
"dependencies": ["."],
"graphs": {
"chatbot": "./agent.py:graph"
}
}
FieldDescription
dependenciesPython path(s) to add (usually ["."] for current directory)
graphsMap of graph names to file:variable paths

policies/domain.yml

The chatbot template includes a real, four-phase policy — not a grant-all bypass:

  • Operation phase: Validates that requests are authenticated
  • Resource phase: Allows access to AgentVisor API resources (threads, runs, state, agents, store)
  • HTTP phase: Only allows requests to Ollama endpoints (ollama, localhost:11434, 127.0.0.1:11434)
  • Default: Denies everything not explicitly allowed

This means your agent can talk to Ollama but cannot reach any other endpoint. The chatbot itself has no HTTP tool to point elsewhere, so to see the policy actually deny something, tighten the allowlist and watch the agent's own Ollama call get blocked instead.

Edit policies/domain.yml and remove the localhost:11434 selector line — this template's OLLAMA_BASE_URL defaults to http://localhost:11434, so this is the line actually matching your traffic:

# Ollama HTTP endpoints (selector-based since host doesn't pre-specify group)
- name: ollama-endpoints
description: "Ollama LLM API endpoints"
selector:
- "mrn:agentvisor:http:ollama.*"
# - "mrn:agentvisor:http:localhost:11434.*" # removed to test policy denial
- "mrn:agentvisor:http:127\\.0\\.0\\.1:11434.*"
group: "mrn:agentvisor:resourcegroup:ollama"

Restart the agent (Ctrl+C, then agentvisor serve . again) and send another message:

curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=30s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Hello again"}]}}' \
| jq '.output.messages[-1].content'

The proxy now denies the LLM call before it leaves the host (the proxy responds 403 Forbidden). agent.py's chat() node catches the resulting error and returns it as the assistant's message, so you'll see an error string instead of a normal chat reply — the policy engine, not the agent code, blocked the request. Restore the selector line afterward so the agent works normally again.

See Policy Configuration for details on writing policies.

compose.yml

The compose file starts two development services:

  • Temporal: Workflow orchestration — manages thread lifecycle, checkpointing, and durable execution
  • Ollama: Local LLM inference — pulls the llama3.2:1b model automatically on first start

The Temporal Web UI is available at http://localhost:8233 for inspecting workflows.

Next Steps