Easy Ollama Plugin and Samples for Unity
Tools (Tools / Function Calling) let the LLM access in-game features and data directly. When the LLM decides it needs an action like “check the player’s gold” or “buy an item,” it automatically calls a C# function you registered.
If the player says to an NPC, “I want to buy three health potions”:
BuyItem function is called automatically (BuyItem("health_potion", 3)).This lets NPC conversations trigger real gameplay logic.
Use RegisterTool() to make a C# function available to the LLM:
// Register an addition tool
client.RegisterTool(
name: "add_numbers",
description: "Add two numbers together",
callback: (Func<int, int, int>)((int a, int b) => a + b)
);
Key points:
name: Tool name used by the LLM when calling it.description: Helps the LLM decide when to use the tool.callback: The function to execute (cast to Func<...>).Send messages normally. If the LLM decides a tool is needed, it will call it automatically:
StartCoroutine(client.SendMessageAsync(
"What is 125 + 378?",
response =>
{
Debug.Log(response.Content);
// Example output: "125 + 378 = 503"
}
));
High-level flow of how the LLM and game features connect:
Player -> LLM -> "Tool needed" -> Execute registered function -> Get result -> LLM final response
Detailed flow:
┌─────────────────────────────┐
│ OllamaClient │
│ (with ToolManager) │
├─────────────────────────────┤
│ - RegisterTool() │ <- Tool registration
│ - SendMessageAsync() │ <- Send message
│ - Tool auto-execution │ <- LLM decides to call tools
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Ollama API │
├─────────────────────────────┤
│ - Request includes tools │
│ - tool_calls in response │
│ - Re-request with results │
└─────────────────────────────┘
The simplest example. One parameter, and return values are converted automatically.
// Tool that evaluates a math expression
client.RegisterTool(
name: "calculator",
description: "Evaluate a math expression like '5+3' or '10*2'",
callback: (Func<string, object>)((string expression) =>
{
try
{
// Example implementation using DataTable
return new System.Data.DataTable().Compute(expression, null);
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
})
);
// Example
// Player: "What is 5 + 3?"
// LLM calls calculator("5+3") -> 8 -> "5 + 3 is 8."
Notes:
object return values are auto-stringified.Multiple parameters with optional defaults.
var client = new OllamaClient(config);
// Tool registration: calculator
// -> inputSchema auto-generated via reflection
// -> return value auto-converted (no ToString required)
client.RegisterTool(
name: "calculator",
description: "Evaluate a math expression",
callback: (Func<string, object>)((string expression) =>
{
try
{
return new System.Data.DataTable().Compute(expression, null);
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
})
);
// Web search tool (maxResults is optional)
client.RegisterTool(
name: "search_web",
description: "Search the web for information",
callback: (Func<string, int, string>)((string query, int maxResults = 5) =>
{
var results = SearchEngine.Search(query, maxResults);
return $"Found {results.Count} results for '{query}'";
})
);
// Example
// Player: "Search Unity news for 3 items"
// LLM calls search_web("Unity", 3)
// Player: "Search about Python"
// LLM calls search_web("Python") with default maxResults=5
Notes:
required.Use [ToolParameter] to improve the LLM’s accuracy.
using EasyLocalLLM.LLM.Core;
// Weather tool
client.RegisterTool(
name: "get_weather",
description: "Get current weather information for a city",
callback: (Func<string, string, string>)((
[ToolParameter("City name (e.g., Tokyo, New York)")] string city,
[ToolParameter("Temperature unit: celsius or fahrenheit")] string unit = "celsius"
) =>
{
var weather = WeatherAPI.GetWeather(city, unit);
return $"Weather in {city}: {weather.Temperature} degrees {(unit == "celsius" ? "C" : "F")}, {weather.Condition}";
})
);
// Example
// Player: "What's the weather in Tokyo in Fahrenheit?"
// LLM calls get_weather("Tokyo", "fahrenheit")
Notes:
[ToolParameter] clarifies each parameter.Return values like int, bool, and double are automatically converted to strings.
// Addition tool (int return)
client.RegisterTool(
name: "add_numbers",
description: "Add two numbers together",
callback: (Func<int, int, int>)((int a, int b) => a + b)
);
// Even check tool (bool return)
client.RegisterTool(
name: "is_even",
description: "Check if a number is even",
callback: (Func<int, bool>)((int number) => number % 2 == 0)
);
// Division tool (double return)
client.RegisterTool(
name: "divide",
description: "Divide two numbers",
callback: (Func<double, double, double>)((double a, double b) => a / b)
);
// Example
// Player: "What is 10 + 5?"
// LLM: add_numbers(10, 5) -> 15 -> "15"
// Player: "Is 8 even?"
// LLM: is_even(8) -> true -> "Yes, 8 is even."
**Notes:**
- No need to call `.ToString()`.
- The library converts values automatically.
### Pattern 5: Custom Object Return Types (JSON Conversion)
Objects and arrays are automatically serialized to JSON strings.
```csharp
// Player info tool (returns object)
client.RegisterTool(
name: "get_player_info",
description: "Get player information by player ID",
callback: (Func<string, object>)((string playerId) =>
{
var player = PlayerManager.GetPlayer(playerId);
return new
{
id = player.Id,
name = player.Name,
level = player.Level,
gold = player.Gold,
health = new { current = player.HP, max = player.MaxHP }
};
})
);
// Inventory tool (returns array)
client.RegisterTool(
name: "get_inventory",
description: "Get all items in player's inventory",
callback: (Func<List<object>>)(() =>
{
return Inventory.GetAllItems().Select(item => new
{
id = item.Id,
name = item.Name,
quantity = item.Quantity
}).ToList();
})
);
// Example
// Player: "Show my status"
// LLM: get_player_info("player1") -> JSON string -> LLM turns into natural text
Notes:
public class NPCShopkeeper : MonoBehaviour
{
private OllamaClient client;
void Start()
{
client = LLMClientFactory.CreateOllamaClient(config);
// Fetch shop inventory
client.RegisterTool(
name: "GetShopItems",
description: "Get list of items available in the shop",
callback: (Func<List<ShopItem>>)(() => ShopManager.GetAvailableItems())
);
// Buy an item
client.RegisterTool(
name: "BuyItem",
description: "Buy an item from the shop",
callback: (Func<string, int, string>)((string itemName, int quantity) =>
{
var result = ShopManager.BuyItem(itemName, quantity);
if (result.Success)
return $"Purchased {quantity}x {itemName}. Remaining gold: {Player.Gold}";
else
return $"Error: {result.ErrorMessage}";
})
);
}
}
// Example
// Player: "What's for sale?"
// LLM calls GetShopItems() and explains the list
//
// Player: "Give me 3 health potions"
// LLM calls BuyItem("health_potion", 3)
When you register a tool, you do not need to write inputSchema manually. The library uses reflection to generate it automatically.
| C# Type | JSON Schema Type | Notes |
|---|---|---|
string |
string |
String |
int, long |
integer |
Integer |
double, float |
number |
Floating point |
bool |
boolean |
Boolean |
List<T>, T[] |
array |
Array |
DateTime |
string |
ISO 8601 |
Guid |
string |
GUID string |
Code:
client.RegisterTool(
name: "search",
description: "Search for items",
callback: (Func<string, int, string>)((string query, int maxResults = 5) => "...")
);
Generated schema:
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "query"
},
"maxResults": {
"type": "integer",
"description": "maxResults"
}
},
"required": ["query"]
}
maxResults has a default value, so it is excluded from required.
For complex inputs (nested objects, etc.), you can define the schema manually:
client.RegisterTool(
name: "create_character",
description: "Create a new game character",
inputSchema: new
{
type = "object",
properties = new
{
name = new { type = "string", description = "Character name" },
stats = new
{
type = "object",
properties = new
{
strength = new { type = "integer", minimum = 1, maximum = 10 },
agility = new { type = "integer", minimum = 1, maximum = 10 }
},
required = new[] { "strength", "agility" }
}
},
required = new[] { "name", "stats" }
},
callback: (Func<string, string>)((string json) =>
{
// Parse JSON manually
var data = JObject.Parse(json);
// ...
return "Character created";
})
);
For more manual schema examples, see InputSchema_Examples.md.
1. Register tools
RegisterTool() registers functions
↓
2. Send message
Call SendMessageAsync("Question")
↓
3. LLM decides
"This question needs a tool"
↓
4. Tool auto-execution
Call the registered function
↓
5. Return results to LLM
Re-request with tool results
↓
6. Final response
LLM produces a natural reply
[User] SendMessageAsync("What is 125 + 378?")
↓
[OllamaClient] Build request
- message: "What is 125 + 378?"
- available tools: [add_numbers]
↓
[Ollama API] returns tool_call
- tool_name: "add_numbers"
- arguments: {"a": 125, "b": 378}
↓
[ToolManager] Execute tool
- Convert JSON arguments to C# types
- callback(125, 378)
- return value: 503 (int)
- int -> "503" (string)
↓
[OllamaClient] Re-request with result
- message history
- user: "What is 125 + 378?"
- tool_result: "503"
↓
[Ollama API] Final response
- "125 + 378 = 503"
↓
[User] Receives ChatResponse
The LLM can call tools multiple times if needed:
Player: "Buy 3 health potions and tell me the remaining gold"
↓
LLM: BuyItem("health_potion", 3)
→ "Purchase successful. Gold left: 450G"
↓
LLM: Final response
→ "Bought three health potions. You have 450G left."
To prevent infinite loops, a maximum iteration count is set (default: 5).
Bad:
client.RegisterTool(
name: "func1",
description: "Does something",
callback: ...
);
Good:
client.RegisterTool(
name: "get_player_health",
description: "Get the current and maximum health points of a player by player ID",
callback: ...
);
client.RegisterTool(
name: "move_character",
description: "Move character to a location",
callback: (Func<string, float, float, string>)((
[ToolParameter("Character ID or name")] string characterId,
[ToolParameter("X coordinate")] float x,
[ToolParameter("Y coordinate")] float y
) => { ... })
);
client.RegisterTool(
name: "buy_item",
description: "Purchase an item",
callback: (Func<string, int, string>)((string itemName, int quantity) =>
{
try
{
if (quantity <= 0)
return "Error: Quantity must be positive";
if (!Shop.HasItem(itemName))
return $"Error: Item '{itemName}' not found in shop";
if (Player.Gold < Shop.GetPrice(itemName) * quantity)
return "Error: Not enough gold";
Shop.BuyItem(itemName, quantity);
return $"Success: Purchased {quantity}x {itemName}";
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
})
);
Auto schema generation works best with primitive types like string, int, and bool.
Recommended:
callback: (Func<string, int, string>)((string itemName, int quantity) => ...)
For complex types, use a manual schema:
// For custom classes, use manual schema and parse JSON manually
callback: (Func<string, string>)((string json) =>
{
var data = JsonConvert.DeserializeObject<ComplexData>(json);
...
})
Enable DebugMode = true to log the auto-generated schema during development.
var config = new OllamaConfig
{
DebugMode = true
};
Cause:
Fix:
var options = new ChatRequestOptions
{
SystemPrompt = "You are a helpful assistant. " +
"When the user asks about their inventory, use the get_inventory tool. " +
"When they want to buy items, use the buy_item tool."
};
Cause:
Fix:
DebugMode[ToolParameter]callback: (Func<string, int, string>)((
[ToolParameter("Player ID (must be a string)")] string playerId,
[ToolParameter("Amount (must be a positive integer)")] int amount
) => ...)
Cause:
Fix:
MaxToolIterationsvar options = new ChatRequestOptions
{
MaxToolIterations = 3 // Default is 5
};
Cause:
Fix:
client.RegisterTool(
name: "complex_tool",
description: "Tool with complex input",
inputSchema: new { /* manual schema */ },
callback: (Func<string, string>)((string json) =>
{
var data = JObject.Parse(json);
// ...
})
);
See InputSchema_Examples.md for more details.
With EasyLocalLLM tools:
Next steps: