import Anthropic from '@anthropic-ai/sdk';
const apiKey = process.env.CLAUDE_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error('CLAUDE_API_KEY or ANTHROPIC_API_KEY environment variable is required');
}
const anthropic = new Anthropic({
apiKey,
});
export async function interpretNaturalLanguage(query: string): Promise<{
filters?: string;
searchQuery?: string;
sortBy?: string;
indexName?: string;
}> {
const prompt = `
You are an assistant that converts natural language search queries into Algolia-compatible filters for a Pokemon battle strategy application.
You can search across different Pokemon indices and should interpret the user's intent to determine:
1. What to search for (searchQuery)
2. How to filter results (filters)
3. How to sort results (sortBy)
4. Which specific index to search (indexName, if mentioned)
Available indices and their filters:
pokemon index:
types (Fire, Water, Grass, Electric, Psychic, Dragon, Steel, Fairy, etc.)
battleRole (Mixed Sweeper, Special Sweeper, Physical Sweeper, Tank, Wall, Support, etc.)
competitiveTier (OU, UU, RU, NU, PU, Uber, LC, etc.)
generation (1-9)
region (Kanto, Johto, Hoenn, Sinnoh, Unova, Kalos, Alola, Galar, Paldea)
category (Genetic Pokemon, Flame Pokemon, Seed Pokemon, etc.)
abilities (Pressure, Levitate, Flash Fire, etc.)
hiddenAbility (ability name)
keyMoves (array of signature moves)
evolutionLine (array of Pokemon names in evolution chain)
stats.hp, stats.attack, stats.defense, stats.specialAttack, stats.specialDefense, stats.speed, stats.total (numeric)
number, height, weight, evolutionStage (numeric)
legendary, isStarter (true/false)
typeEffectiveness.weakTo, typeEffectiveness.resistantTo, typeEffectiveness.immuneTo (arrays)
pokemon_moves index:
type (Normal, Fire, Water, Electric, etc.)
category (Physical, Special, Status)
targets (Single target, All adjacent, etc.)
tm (TM number or "None")
learnedBy (array of Pokemon names)
power, accuracy, pp, priority, generation (numeric)
battleUtility.damage (low, moderate, high, extreme)
battleUtility.utility (status, healing, suicide, etc.)
battleUtility.coverage (array of coverage types)
competitiveUsage.tier (OU, UU, RU, etc.)
competitiveUsage.popularity, competitiveUsage.commonUsers (numeric and array)
abilities index:
generation (1-9)
isHidden (true/false)
competitiveRating (1-10)
pokemonWithAbility (array of Pokemon names)
items index:
category (Healing, Offensive, Defensive, Status, etc.)
competitiveViability (S, A, B, C, D)
isConsumable (true/false)
usagePercent (0-100)
competitive_stats index:
pokemon (Pokemon name)
tier (OU, UU, RU, etc.)
format (gen1ou, gen2ou, etc.)
month (YYYY-MM format)
usagePercent, wins, losses (numeric)
commonTeammates, commonCounters (arrays of Pokemon names)
commonMoves, commonItems, commonAbilities (arrays with usage percentages)
team_compositions index:
teamName (string)
pokemon.name, pokemon.role, pokemon.moves (nested arrays)
competitiveViability.tier (OU, UU, etc.)
competitiveViability.winRate, competitiveViability.usage (numeric and percentage)
typeCoverage.coverageScore (0-100)
typeCoverage.offensive, typeCoverage.defensive.weaknesses, typeCoverage.defensive.resistances (arrays)
synergies, commonThreats, suggestions (arrays of descriptions)
team_synergies index:
teamId (identifier)
pokemon (array of Pokemon names)
teamRole (core role description)
strategicFocus (strategy type)
competitiveViability (tier rating)
typeCoverage.coverageScore, typeCoverage.weaknesses, typeCoverage.resistances (numeric and arrays)
synergies, commonCounters, movesetSynergy (arrays)
meta_analysis index:
tier (OU, UU, RU, etc.)
month (YYYY-MM format)
topPokemon.name, topPokemon.usage, topPokemon.rank (nested data)
topPokemon.sets.name, topPokemon.sets.moves, topPokemon.sets.item, topPokemon.sets.ability (nested arrays)
trends.rising, trends.falling, trends.stable (arrays)
threats.most_used_moves, threats.common_strategies (arrays)
recommendations.emerging_picks, recommendations.meta_shifts (arrays)
type_effectiveness index:
attackingType, defendingType (type names)
effectiveness (0, 0.5, 1, 2)
FILTERING EXAMPLES:
Fast Electric types: types:Electric AND stats.speed >= 100
OU legendary Pokemon: competitiveTier:OU AND legendary:true
OU Pokemon: competitiveTier:OU
UU tier Pokemon: competitiveTier:UU
RU tier Pokemon: competitiveTier:RU
Uber tier Pokemon: competitiveTier:Uber
Physical moves over 100 power: category:Physical AND power >= 100
Teams using Charizard: pokemon.name:Charizard
Current OU meta trends: tier:OU AND month:[latest]
Fire-resistant Pokemon: typeEffectiveness.resistantTo:Fire
Use operators: AND, OR, NOT, <, >, <=, >=, :
Examples: "types:Fire", "stats.speed>100", "battleRole:sweeper AND competitiveTier:OU"
User query: "${query}"
Analyze the query and extract:
- searchQuery: The main search terms (Pokemon names, move names, ability names)
- filters: Algolia filter string for specific attributes
- sortBy: Sorting preference (speed, attack, total, etc.)
- indexName: Specific index if mentioned, otherwise leave empty for default behavior
Respond ONLY with valid JSON like:
{
"searchQuery": "Charizard",
"filters": "types:FireAI-Powered Battle Assistant",
"indexName": "pokemon"
}
Or
{
"searchQuery": "",
"filters": "keyMoves:'' AND stats.speed > 100",
"indexName": "pokemon"
}
Or for simpler queries:
{
"searchQuery": "Pikachu",
"filters": "types:Electric",
"indexName": "pokemon"
}
`;
const msg = await anthropic.messages.create({
model: "claude-opus-4-20250514",
max_tokens: 300,
temperature: 0.2,
messages: [
{
role: 'user',
content: prompt,
},
],
});
const firstBlock = msg.content[0];
let text = firstBlock?.type === 'text' ? firstBlock.text.trim() : '';
// Remove markdown code block formatting if present
if (text.startsWith('```json') && text.endsWith('```')) {
text = text.slice(7, -3).trim();
} else if (text.startsWith('```') && text.endsWith('```')) {
text = text.slice(3, -3).trim();
}
try {
return JSON.parse(text ?? '');
} catch (err) {
console.error('Failed to parse Claude response:', text);
throw new Error('Claude returned malformed JSON');
}
}
export async function summarizeResultsWithClaude(query: string, results: any[]): Promise<string> {
// The results are already flattened individual hits, not objects with .hits properties
const flattenedHits = results.slice(0, 10); // limit for context
console.log("Results being summarized:", flattenedHits.length, "items");
if (flattenedHits.length === 0) {
return `No results found for "${query}". Try searching for specific Pokemon names, types, or battle strategies.`;
}
const summaryPrompt = `
You are an assistant that helps users understand search results from multiple sources.
User asked: "${query}"
Here are the top results from multiple datasets:
${JSON.stringify(flattenedHits, null, 2)}
Summarize the results in a helpful, concise answer for the user. Be conversational, clear and concise. Do not use emojis.
`;
const msg = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 300,
temperature: 0.2,
messages: [
{
role: 'user',
content: summaryPrompt,
},
],
});
const firstBlock = msg.content[0];
return firstBlock?.type === 'text' ? firstBlock.text.trim() : '';
}