search_equipment.py•13.3 kB
"""Equipment search tool using the repository pattern for caching.
This module provides comprehensive equipment search for weapons, armor, and magic
items with automatic database caching through the repository pattern. The repository
abstracts away cache management and allows filtering across multiple equipment types.
Architecture:
- Uses EquipmentRepository for cache-aside pattern with item-type routing
- Repository manages Milvus cache automatically
- Supports test context-based repository injection
- Handles weapon, armor, and magic item filtering
Examples:
Default usage (automatically creates repository):
weapons = await search_equipment(type="weapon", damage_dice="1d8")
items = await search_equipment(type="magic-item", rarity="rare")
With context-based injection (testing):
from lorekeeper_mcp.tools.search_equipment import _repository_context
from lorekeeper_mcp.repositories.equipment import EquipmentRepository
repository = EquipmentRepository(cache=my_cache)
_repository_context["repository"] = repository
armor = await search_equipment(type="armor")
Item type filtering:
all_items = await search_equipment(type="all", name="chain")
simple_weapons = await search_equipment(type="weapon", is_simple=True)"""
from typing import Any, Literal, cast
from lorekeeper_mcp.repositories.equipment import EquipmentRepository
from lorekeeper_mcp.repositories.factory import RepositoryFactory
_repository_context: dict[str, Any] = {}
EquipmentType = Literal["weapon", "armor", "magic-item", "all"]
def _get_repository() -> EquipmentRepository:
"""Get equipment repository, respecting test context.
Returns the repository from _repository_context if set, otherwise creates
a default EquipmentRepository using RepositoryFactory.
Returns:
EquipmentRepository instance for equipment lookups.
"""
if "repository" in _repository_context:
return cast(EquipmentRepository, _repository_context["repository"])
return RepositoryFactory.create_equipment_repository()
async def search_equipment(
type: EquipmentType = "all", # noqa: A002
rarity: str | None = None,
damage_dice: str | None = None,
is_simple: bool | None = None,
requires_attunement: str | None = None,
cost_min: int | float | None = None,
cost_max: int | float | None = None,
weight_max: float | None = None,
is_finesse: bool | None = None,
is_light: bool | None = None,
is_magic: bool | None = None,
documents: list[str] | None = None, # Replaces document and document_keys
search: str | None = None,
limit: int = 20,
) -> list[dict[str, Any]]:
"""
Search and retrieve D&D 5e weapons, armor, and magic items using the repository pattern.
This tool provides comprehensive equipment lookup across weapons, armor, and magical
items. Filter by rarity, damage potential, complexity, or attunement requirements.
Automatically uses the database cache through the repository for improved performance.
Examples:
Basic equipment lookup:
rare_items = await search_equipment(type="magic-item", rarity="rare")
light_armor = await search_equipment(type="armor", is_simple=True)
Using cost ranges (NEW in Phase 3):
affordable_weapons = await search_equipment(
type="weapon", cost_max=25
)
expensive_items = await search_equipment(
type="weapon", cost_min=50, cost_max=100
)
Using weight and properties (NEW in Phase 3):
lightweight_weapons = await search_equipment(
type="weapon", weight_max=3
)
finesse_weapons = await search_equipment(
type="weapon", is_finesse=True
)
light_dual_wield_weapons = await search_equipment(
type="weapon", is_light=True
)
magical_weapons = await search_equipment(
type="weapon", is_magic=True
)
Complex equipment queries:
affordable_simple_weapons = await search_equipment(
type="weapon", is_simple=True, cost_max=10
)
light_finesse_weapons = await search_equipment(
type="weapon", is_light=True, is_finesse=True, limit=10
)
expensive_magical_weapons = await search_equipment(
type="weapon", is_magic=True, cost_min=100
)
Searching all types:
all_chain_items = await search_equipment(
type="all", search="chain"
)
Semantic search (natural language queries):
melee_weapons = await search_equipment(
type="weapon", search="slashing blade for close combat"
)
protective_gear = await search_equipment(
type="armor", search="heavy protective plate"
)
magical_storage = await search_equipment(
type="magic-item", search="bag that holds items"
)
Hybrid search (search + filters):
finesse_slashing = await search_equipment(
type="weapon", search="elegant blade", is_finesse=True
)
rare_magical = await search_equipment(
type="magic-item", search="fire wand", rarity="rare"
)
Args:
type: Equipment type to search. Default "all" searches all types. Options:
- "weapon": Melee weapons (longsword, dagger, etc.) and ranged weapons (bow, crossbow)
- "armor": Protective gear (leather armor, chain mail, plate, etc.)
- "magic-item": Magical items (Bag of Holding, Wand of Fireballs, etc.)
- "all": Search all equipment types simultaneously (may return many results)
rarity: Magic item rarity filter (weapon/armor types don't use this).
Valid values: common, uncommon, rare, very rare, legendary, artifact
Example: "rare" for high-value magical items
damage_dice: Weapon damage dice filter to find weapons dealing specific damage.
Examples: "1d4" (dagger), "1d8" (longsword), "2d6" (greataxe), "1d12" (greatsword)
is_simple: Filter for simple weapons (True) or martial weapons (False).
Simple weapons: club, dagger, greatclub, handaxe, javelin, light hammer, mace,
quarterstaff, sickle, spear
Martial weapons: all other melee and ranged weapons
Example: True for low-complexity options
requires_attunement: Magic item attunement filter. Some powerful items require
attunement to a character. Examples: "yes", "no", or specific requirements
cost_min: Minimum cost in gold pieces (weapons and armor). Filters items costing
at least this amount. Example: 10 for items costing 10+ gp
cost_max: Maximum cost in gold pieces (weapons and armor). Filters items costing
at most this amount. Example: 25 for items costing 25 gp or less
weight_max: Maximum weight in pounds (weapons). Filters weapons weighing at most
this amount. Example: 3 for lightweight weapons
is_finesse: Finesse property filter (weapons). When True, returns only weapons
with the finesse property (can use STR or DEX modifier). Example: True
is_light: Light property filter (weapons). When True, returns only light weapons
suitable for dual-wielding. Example: True
is_magic: Magic property filter (weapons). When True, returns only magical weapons.
Example: True
documents: Filter to specific source documents. Provide a list of
document names/identifiers from list_documents() tool. Examples:
["srd-5e"] for SRD only, ["srd-5e", "tce"] for SRD and Tasha's.
Use list_documents() to see available documents.
search: Natural language search query for semantic/vector search.
When provided, uses vector similarity to find equipment matching the
conceptual meaning rather than exact text matches. Can be combined
with other filters for hybrid search. Examples: "slashing weapon for
melee combat", "protective heavy armor", "magical wand for spells"
limit: Maximum number of results to return. Default 20. For type="all" with many
matches, limit applies to total results. Examples: 5, 20, 100
Returns:
List of equipment dictionaries. Structure varies by type:
For type="weapon":
- name: Weapon name
- damage_dice: Damage expression (e.g., "1d8")
- damage_type: Type of damage (slashing, piercing, bludgeoning)
- weight: Weight in pounds
- is_simple: Whether this is a simple weapon
- range: Range for ranged weapons (e.g., "20/60 feet")
- properties: Weapon properties (finesse, heavy, reach, two-handed, etc.)
- rarity: Equipment rarity
For type="armor":
- name: Armor name
- armor_class: AC provided by this armor
- armor_class_dex: Whether DEX bonus applies (light/medium)
- armor_class_strength: Whether STR requirement applies (heavy)
- weight: Weight in pounds
- armor_category: Light/Medium/Heavy classification
- rarity: Equipment rarity
For type="magic-item":
- name: Item name
- description: What the item does and its powers
- rarity: Rarity level (common through artifact)
- requires_attunement: Attunement requirements
- wondrous: Whether item is wondrous (non-weapon/armor)
- weight: Weight if applicable
- armor_class: AC bonus if armor
- damage: Damage if weapon
Raises:
ApiError: If the API request fails due to network issues or server errors
"""
repository = _get_repository()
results: list[dict[str, Any]] = []
if type in ("weapon", "all"):
weapon_filters: dict[str, Any] = {"item_type": "weapon"}
if damage_dice is not None:
weapon_filters["damage_dice"] = damage_dice
if is_simple is not None:
weapon_filters["is_simple"] = is_simple
if cost_min is not None:
weapon_filters["cost_min"] = cost_min
if cost_max is not None:
weapon_filters["cost_max"] = cost_max
if weight_max is not None:
weapon_filters["weight_max"] = weight_max
if is_finesse is not None:
weapon_filters["is_finesse"] = is_finesse
if is_light is not None:
weapon_filters["is_light"] = is_light
if is_magic is not None:
weapon_filters["is_magic"] = is_magic
if documents is not None:
weapon_filters["document"] = documents
if search is not None:
weapon_filters["search"] = search
weapons = await repository.search(limit=limit, **weapon_filters)
weapon_dicts = [w.model_dump() for w in weapons]
results.extend(weapon_dicts)
if type in ("armor", "all"):
armor_filters: dict[str, Any] = {"item_type": "armor"}
if cost_min is not None:
armor_filters["cost_min"] = cost_min
if cost_max is not None:
armor_filters["cost_max"] = cost_max
if documents is not None:
armor_filters["document"] = documents
if search is not None:
armor_filters["search"] = search
armors = await repository.search(limit=limit, **armor_filters)
armor_dicts = [a.model_dump() for a in armors]
results.extend(armor_dicts)
if type in ("magic-item", "all"):
magic_item_filters: dict[str, Any] = {"item_type": "magic-item"}
if rarity is not None:
magic_item_filters["rarity"] = rarity
if requires_attunement is not None:
if requires_attunement.lower() in ("yes", "true", "1"):
magic_item_filters["requires_attunement"] = True
else:
magic_item_filters["requires_attunement"] = False
if documents is not None:
magic_item_filters["document"] = documents
if search is not None:
magic_item_filters["search"] = search
magic_items = await repository.search(limit=limit, **magic_item_filters)
magic_item_dicts = [m.model_dump() for m in magic_items]
results.extend(magic_item_dicts)
if type == "all" and len(results) > limit:
results = results[:limit]
return results