poe2-agent 0.5.0

AI agent for Path of Exile 2 build analysis
Documentation
//! Trade API tools — search_trade and check_currency_price.

use async_trait::async_trait;

use crate::llm::ToolDefinition;
use crate::trade::SearchParams;

use super::{parse_args, Tool, ToolContext, ToolResult};

/// Register trade tools.
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())
    }
}