use async_trait::async_trait;
use crate::llm::ToolDefinition;
use crate::pob_parser::PobQuery;
use super::{parse_args, pob_query, Tool, ToolContext, ToolResult};
pub fn register(tools: &mut Vec<Box<dyn Tool>>) {
tools.push(Box::new(GetBuildStats));
tools.push(Box::new(GetSkillList));
tools.push(Box::new(GetConfig));
tools.push(Box::new(GetEquippedItems));
tools.push(Box::new(GetPassiveTree));
tools.push(Box::new(GetUnallocatedAscendancy));
tools.push(Box::new(ListCharms));
tools.push(Box::new(GetItem));
tools.push(Box::new(AnalyzeGearMods));
tools.push(Box::new(GetJewel));
tools.push(Box::new(QueryPassiveStats));
tools.push(Box::new(GetSkillBreakdown));
tools.push(Box::new(SearchGems));
tools.push(Box::new(SearchUniques));
tools.push(Box::new(SearchRunes));
tools.push(Box::new(SearchBases));
tools.push(Box::new(SearchMods));
}
macro_rules! simple_pob_tool {
($struct_name:ident, $tool_name:literal, $description:literal, $query:expr) => {
struct $struct_name;
#[async_trait]
impl Tool for $struct_name {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: $tool_name.to_owned(),
description: $description.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}),
}
}
async fn execute(
&self,
ctx: &ToolContext<'_>,
_args: &str,
) -> Result<ToolResult, String> {
pob_query(ctx, $query).await
}
}
};
}
simple_pob_tool!(
GetBuildStats,
"get_build_stats",
"Get extended build statistics including offense, defense, \
resources, speed, and charges. Returns ~40 fields grouped by category.",
PobQuery::BuildStats
);
simple_pob_tool!(
GetSkillList,
"get_skill_list",
"Get the list of skills with their DPS values, trigger info, \
and gem links (socket groups with gems, levels, and quality).",
PobQuery::SkillList
);
simple_pob_tool!(
GetConfig,
"get_config",
"Get the build's configuration flags (enemy settings, \
charge generation, conditions, etc.).",
PobQuery::Config
);
simple_pob_tool!(
GetEquippedItems,
"get_equipped_items",
"Get all equipped items across every gear slot in one call. \
Returns compact info for each slot (name, base, rarity, mod lines), \
marks empty slots, and includes socketed jewels. Use this for broad \
gear questions before diving into specific items with get_item.",
PobQuery::EquippedItems
);
simple_pob_tool!(
GetPassiveTree,
"get_passive_tree",
"Get the allocated passive tree nodes, grouped by type: \
keystones, notables, ascendancy nodes, masteries, and jewel sockets. \
Also returns class, ascendancy, and total allocated node count.",
PobQuery::PassiveTree
);
simple_pob_tool!(
GetUnallocatedAscendancy,
"get_unallocated_ascendancy",
"Get the character's ascendancy nodes — both allocated and \
available — for primary and secondary ascendancies. Returns node names, \
types, stats, and points spent. Use this to recommend which ascendancy \
nodes to take next.",
PobQuery::UnallocatedAscendancy
);
simple_pob_tool!(
ListCharms,
"list_charms",
"List all PoE2 charm bases with their trigger condition, buff \
effect, duration, and charge info. Charms auto-activate when a condition \
is met (e.g. becoming Frozen). Returns all 13 charms — no parameters \
needed.",
PobQuery::ListCharms
);
struct GetItem;
#[async_trait]
impl Tool for GetItem {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "get_item".to_owned(),
description: "Retrieve the item equipped in a specific gear slot, including \
its name, base type, rarity, and all mod lines (implicit, explicit, \
enchant, rune)."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"slot": {
"type": "string",
"enum": [
"Weapon 1", "Weapon 2", "Helmet", "Body Armour",
"Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Ring 3",
"Belt", "Charm 1", "Charm 2", "Charm 3",
"Flask 1", "Flask 2"
],
"description": "The equipment slot to inspect"
}
},
"required": ["slot"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let slot = args["slot"]
.as_str()
.ok_or("missing required parameter: slot")?
.to_owned();
pob_query(ctx, PobQuery::Item(slot)).await
}
}
struct AnalyzeGearMods;
#[async_trait]
impl Tool for AnalyzeGearMods {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "analyze_gear_mods".to_owned(),
description: "Analyze an item's mods in depth: tier info (T1-T13), roll \
quality (0-100%), affix names, current vs max range, and whether \
better tiers are available at the item's level. Shows open prefix/suffix \
slots. Not applicable to unique items, flasks, or charms."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"slot": {
"type": "string",
"enum": [
"Weapon 1", "Weapon 2", "Helmet", "Body Armour",
"Gloves", "Boots", "Amulet", "Ring 1", "Ring 2", "Ring 3",
"Belt"
],
"description": "The equipment slot to analyze (excludes Charms and Flasks)"
}
},
"required": ["slot"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let slot = args["slot"]
.as_str()
.ok_or("missing required parameter: slot")?
.to_owned();
pob_query(ctx, PobQuery::GearModAnalysis(slot)).await
}
}
struct GetJewel;
#[async_trait]
impl Tool for GetJewel {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "get_jewel".to_owned(),
description: "Retrieve a jewel socketed in a passive tree socket, including \
its name, base type, rarity, and all mod lines. Use socket node IDs \
from get_passive_tree's jewel_sockets array."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"node_id": {
"type": "integer",
"description": "The passive tree socket node ID (from get_passive_tree jewel_sockets)"
}
},
"required": ["node_id"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let node_id = args["node_id"]
.as_i64()
.ok_or("missing required parameter: node_id")?;
pob_query(ctx, PobQuery::Jewel(node_id)).await
}
}
struct QueryPassiveStats;
#[async_trait]
impl Tool for QueryPassiveStats {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "query_passive_stats".to_owned(),
description: "Query how much of one or more stats comes from allocated passive \
tree nodes, and how much more is available on nearby unallocated nodes. \
Accepts a list of stat patterns to batch multiple lookups in one call. \
Uses case-insensitive pattern matching on stat descriptions."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"stats": {
"type": "array",
"items": { "type": "string" },
"description": "Stat patterns to search for (e.g. [\"fire damage\", \"maximum life\", \"critical strike\"])"
},
"radius": {
"type": "integer",
"description": "How many hops from allocated nodes to search for nearby stats (default: 3)"
}
},
"required": ["stats"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let stats = if let Some(arr) = args["stats"].as_array() {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
.collect::<Vec<_>>()
} else if let Some(s) = args["stat"].as_str() {
vec![s.to_owned()]
} else {
return Err("missing required parameter: stats".to_owned());
};
let radius = args["radius"].as_u64().unwrap_or(3) as u32;
pob_query(ctx, PobQuery::PassiveStats { stats, radius }).await
}
}
struct GetSkillBreakdown;
#[async_trait]
impl Tool for GetSkillBreakdown {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "get_skill_breakdown".to_owned(),
description: "Get a detailed DPS breakdown for a specific skill, including \
average hit, crit stats, speed, per-damage-type min/max, damage conversions, \
and ailment DPS. Recalculates with the skill as main skill. Use this when \
the user asks why a skill's DPS is low or wants to understand damage \
composition."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"skill": {
"type": "string",
"description": "The skill name to look up (case-insensitive substring match)"
}
},
"required": ["skill"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let skill = args["skill"]
.as_str()
.ok_or("missing required parameter: skill")?
.to_owned();
pob_query(ctx, PobQuery::SkillBreakdown(skill)).await
}
}
struct SearchGems;
#[async_trait]
impl Tool for SearchGems {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_gems".to_owned(),
description: "Search the PoE2 gem database by name, type, and/or tags. \
Returns matching gems with their type, tags, attribute requirements, \
and tier. Use this to find gems that match specific criteria instead \
of guessing from memory. Supports filtering by active vs support and \
by tags like projectile, fire, area, duration, etc."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Case-insensitive name substring (e.g. \"nova\", \"fire\")"
},
"type": {
"type": "string",
"enum": ["active", "support"],
"description": "Filter by active skills or support gems"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Tags that must all match (e.g. [\"projectile\", \"fire\"])"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let query = args["query"].as_str().map(|s| s.to_owned());
let gem_type = args["type"].as_str().map(|s| s.to_owned());
let tags = args["tags"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
if query.is_none() && tags.is_empty() {
return Err("at least one of 'query' or 'tags' must be provided".to_owned());
}
pob_query(
ctx,
PobQuery::SearchGems {
query,
gem_type,
tags,
},
)
.await
}
}
struct SearchUniques;
#[async_trait]
impl Tool for SearchUniques {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_uniques".to_owned(),
description: "Search the PoE2 unique item database by name, slot, and/or \
level range. Returns matching uniques with their base type, slot, \
level requirement, and mod lines. Use this to find unique items that \
match specific criteria instead of guessing from memory."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Case-insensitive substring matched against item name and mod text"
},
"slot": {
"type": "string",
"description": "Filter by slot type (e.g. \"Ring\", \"Helmet\", \"Body Armour\", \"Shield\", \"Axe\", \"Bow\"). Case-insensitive substring match."
},
"min_level": {
"type": "integer",
"description": "Minimum level requirement"
},
"max_level": {
"type": "integer",
"description": "Maximum level requirement"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let query = args["query"].as_str().map(|s| s.to_owned());
let slot = args["slot"].as_str().map(|s| s.to_owned());
let min_level = args["min_level"].as_u64().map(|n| n as u32);
let max_level = args["max_level"].as_u64().map(|n| n as u32);
if query.is_none() && slot.is_none() {
return Err("at least one of 'query' or 'slot' must be provided".to_owned());
}
pob_query(
ctx,
PobQuery::SearchUniques {
query,
slot,
min_level,
max_level,
},
)
.await
}
}
struct SearchRunes;
#[async_trait]
impl Tool for SearchRunes {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_runes".to_owned(),
description: "Search the PoE2 rune and soul core database by name or stat \
text. Runes give different bonuses depending on the equipment slot they're \
socketed into. Returns matching runes with per-slot stat lines including \
bonded effects."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Case-insensitive substring matched against rune name and stat text"
},
"slot": {
"type": "string",
"description": "Filter by equipment slot: weapon, caster, armour, helmet, body armour, boots, gloves, focus, sceptre"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let query = args["query"].as_str().map(|s| s.to_owned());
let slot = args["slot"].as_str().map(|s| s.to_owned());
if query.is_none() && slot.is_none() {
return Err("at least one of 'query' or 'slot' must be provided".to_owned());
}
pob_query(ctx, PobQuery::SearchRunes { query, slot }).await
}
}
struct SearchBases;
#[async_trait]
impl Tool for SearchBases {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_bases".to_owned(),
description: "Search PoE2 item base types by category and/or name. Returns base name, \
type, implicit mods, level requirement, weapon/armour stats, and tags. Use this to \
discover valid base type names before creating items with create_item."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"item_type": {
"type": "string",
"description": "Item type category (e.g. \"Bow\", \"Helmet\", \"Body Armour\", \"Ring\", \"Amulet\", \"Belt\", \"Gloves\", \"Boots\", \"Shield\", \"Quiver\")"
},
"query": {
"type": "string",
"description": "Case-insensitive name substring (e.g. \"shortbow\", \"expert\")"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let item_type = args["item_type"].as_str().map(|s| s.to_owned());
let query = args["query"].as_str().map(|s| s.to_owned());
if item_type.is_none() && query.is_none() {
return Err("at least one of 'item_type' or 'query' must be provided".to_owned());
}
pob_query(ctx, PobQuery::SearchBases { item_type, query }).await
}
}
struct SearchMods;
#[async_trait]
impl Tool for SearchMods {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_mods".to_owned(),
description: "Search PoE2 item mod database for valid prefix/suffix mods. Returns mod ID, \
affix name, stat text with value ranges, level, and group. Use this to find exact mod \
text for create_item — the database shows ranges like '+(5-8) to Strength'; use a \
specific value within the range like '+8 to Strength' in your item text. Filter by \
item type tag (from search_bases tags, e.g. 'bow', 'ring', 'str_armour') to see only \
mods that can roll on that base type."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Case-insensitive substring matched against stat text and affix name (e.g. \"physical damage\", \"attack speed\", \"maximum life\")"
},
"item_type_tag": {
"type": "string",
"description": "Filter to mods with weight > 0 for this tag (e.g. \"bow\", \"ring\", \"str_armour\"). Use tags from search_bases results."
},
"mod_type": {
"type": "string",
"enum": ["prefix", "suffix"],
"description": "Filter by prefix or suffix"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let query = args["query"].as_str().map(|s| s.to_owned());
let item_type_tag = args["item_type_tag"].as_str().map(|s| s.to_owned());
let mod_type = args["mod_type"].as_str().map(|s| s.to_owned());
if query.is_none() && item_type_tag.is_none() {
return Err("at least one of 'query' or 'item_type_tag' must be provided".to_owned());
}
pob_query(
ctx,
PobQuery::SearchMods {
query,
item_type_tag,
mod_type,
},
)
.await
}
}