use async_trait::async_trait;
use crate::llm::ToolDefinition;
use crate::trade::SearchParams;
use super::{parse_args, Tool, ToolContext, ToolResult};
pub fn register(tools: &mut Vec<Box<dyn Tool>>) {
tools.push(Box::new(SearchTrade));
tools.push(Box::new(CheckCurrencyPrice));
}
struct SearchTrade;
#[async_trait]
impl Tool for SearchTrade {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "search_trade".to_owned(),
description: "Search the PoE2 trade site for items. Returns prices, item details, \
and mod lines for the top results. Use this when the user asks about item prices, \
upgrade costs, or what's available on the market."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Item name (for uniques)"
},
"type": {
"type": "string",
"description": "Base type (e.g. 'Leather Vest', 'Expert Shortbow'). Must be a real base type name — do not pass an empty string."
},
"category": {
"type": "string",
"description": "Item category (e.g. 'armour.chest', 'weapon.bow', 'accessory.ring')"
},
"rarity": {
"type": "string",
"enum": ["unique", "rare", "nonunique"],
"description": "Filter by rarity"
},
"stats": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"min": { "type": "number" },
"max": { "type": "number" }
}
},
"description": "Stat filters with human-readable names (e.g. 'maximum life', 'fire resistance')"
},
"max_price": {
"type": "object",
"properties": {
"amount": { "type": "number" },
"currency": { "type": "string" }
},
"description": "Maximum price filter. Only use when the user explicitly requests a budget. Excludes items listed in other currencies."
},
"league": {
"type": "string",
"description": "League name (defaults to current challenge league)"
}
},
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let trade = ctx.trade.ok_or("Trade API is not configured")?;
let args = parse_args(args)?;
let name = args["name"].as_str().map(|s| s.to_owned());
let item_type = args["type"].as_str().map(|s| s.to_owned());
let category = args["category"].as_str().map(|s| s.to_owned());
let rarity = args["rarity"].as_str().map(|s| s.to_owned());
let league = args["league"].as_str().map(|s| s.to_owned());
let stats: Vec<(String, Option<f64>, Option<f64>)> = args["stats"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| {
let name = v["name"].as_str()?.to_owned();
let min = v["min"].as_f64();
let max = v["max"].as_f64();
Some((name, min, max))
})
.collect()
})
.unwrap_or_default();
let max_price = args["max_price"].as_object().and_then(|obj| {
let amount = obj.get("amount")?.as_f64()?;
let currency = obj.get("currency")?.as_str()?.to_owned();
Some((amount, currency))
});
if name.is_none() && item_type.is_none() && category.is_none() && stats.is_empty() {
return Err(
"at least one of 'name', 'type', 'category', or 'stats' must be provided"
.to_owned(),
);
}
let params = SearchParams {
name,
item_type,
category,
rarity,
stats,
max_price,
league,
};
trade
.search(params)
.await
.map(|v| ToolResult {
response: v,
mutation: None,
})
.map_err(|e| e.to_string())
}
}
struct CheckCurrencyPrice;
#[async_trait]
impl Tool for CheckCurrencyPrice {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "check_currency_price".to_owned(),
description: "Check currency exchange rates on the PoE2 trade site. Shows how \
much of one currency you need to buy another."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"have": {
"type": "string",
"description": "Currency to spend (e.g. 'chaos', 'exalted', 'divine')"
},
"want": {
"type": "string",
"description": "Currency to buy (e.g. 'exalted', 'divine', 'regal')"
},
"league": {
"type": "string",
"description": "League name (defaults to current challenge league)"
}
},
"required": ["have", "want"],
"additionalProperties": false
}),
}
}
async fn execute(&self, ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let trade = ctx.trade.ok_or("Trade API is not configured")?;
let args = parse_args(args)?;
let have = args["have"]
.as_str()
.ok_or("missing required parameter: have")?;
let want = args["want"]
.as_str()
.ok_or("missing required parameter: want")?;
let league = args["league"].as_str().map(|s| s.to_owned());
trade
.exchange(have, want, league.as_deref())
.await
.map(|v| ToolResult {
response: v,
mutation: None,
})
.map_err(|e| e.to_string())
}
}