use async_trait::async_trait;
use crate::llm::ToolDefinition;
use crate::pob_parser::PobQuery;
use super::{parse_args, BuildMutation, Tool, ToolContext, ToolResult};
pub fn register(tools: &mut Vec<Box<dyn Tool>>) {
tools.push(Box::new(CreateItem));
tools.push(Box::new(UpdateBuild));
}
struct CreateItem;
#[async_trait]
impl Tool for CreateItem {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "create_item".to_owned(),
description: "Create a custom item from PoB item text format, equip it in a build \
slot, and return the stat impact. Use search_bases first to get valid base type \
names, and search_mods to get valid mod text. The response includes matched_mods \
(count of recognized mods) and unmatched_mods (list of mod lines that PoB didn't \
recognize — these have NO stat effect). If unmatched_mods is non-empty, use \
search_mods to find correct text and re-create.\n\n\
Item text format (newline-separated):\n\
```\n\
Rarity: RARE\n\
Dragon Song <- custom title (Rare/Unique only)\n\
Expert Shortbow <- base type (must match PoE2 base exactly)\n\
Item Level: 86\n\
Quality: 20\n\
Implicits: 1\n\
+15% to Critical Hit Chance <- implicit mod (count set by Implicits: N)\n\
Adds 80 to 120 Physical Damage\n\
+25% to Critical Hit Multiplier\n\
35% increased Attack Speed\n\
```\n\n\
Rarity values: NORMAL, MAGIC, RARE, UNIQUE\n\
Normal/Magic: one name line (base type). Rare/Unique: two lines (title + base).\n\
`Implicits: N` splits mod lines — first N are implicit, rest are explicit.\n\
Use `{custom}` tag for custom mods: `{custom}+1 to Level of all Spell Skills`\n\
Use `{range:0.5}` to set a mid-roll (0.0=min, 1.0=max): `{range:0.5}+50 to maximum Life`\n\n\
If the slot already has an item, it is replaced. Check the slot first with \
`get_item` if you need to know what was there."
.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 slot to equip the new item in"
},
"item_text": {
"type": "string",
"description": "The item in PoB text format (newline-separated). See tool description for format."
}
},
"required": ["slot", "item_text"],
"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();
let item_text = args["item_text"]
.as_str()
.ok_or("missing required parameter: item_text")?
.to_owned();
let mut result = ctx
.parser
.query(
ctx.build_xml,
PobQuery::CreateItem {
slot: slot.clone(),
item_text,
},
)
.await
.map_err(|e| e.to_string())?;
if result.get("error").is_none() {
if let Some(xml) = result
.as_object_mut()
.and_then(|m| m.remove("xml"))
.and_then(|v| v.as_str().map(|s| s.to_owned()))
{
let label = format!("Equipped item in {slot}");
return Ok(ToolResult {
response: result,
mutation: Some(BuildMutation { xml, label }),
});
}
}
Ok(ToolResult {
response: result,
mutation: None,
})
}
}
struct UpdateBuild;
#[async_trait]
impl Tool for UpdateBuild {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
tool_type: "function".to_owned(),
name: "update_build".to_owned(),
description: "Submit an updated build XML to be saved as a new snapshot. Call this \
when you have made changes to the build XML and want to persist them. The XML \
must be a complete, valid PathOfBuilding2 XML document."
.to_owned(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"xml": {
"type": "string",
"description": "The complete updated PathOfBuilding2 XML document."
},
"label": {
"type": "string",
"description": "A short human-readable description of what changed, e.g. 'Swapped Fireball to Lightning Arrow'."
}
},
"required": ["xml", "label"],
"additionalProperties": false
}),
}
}
async fn execute(&self, _ctx: &ToolContext<'_>, args: &str) -> Result<ToolResult, String> {
let args = parse_args(args)?;
let xml = args["xml"]
.as_str()
.ok_or_else(|| "missing xml parameter".to_owned())?
.to_owned();
let label = args["label"].as_str().unwrap_or("Updated build").to_owned();
Ok(ToolResult {
response: serde_json::json!({
"status": "ok",
"message": "Build mutation queued. It will be applied after your response."
}),
mutation: Some(BuildMutation { xml, label }),
})
}
}