Skip to content

Commit

Permalink
🐛 Make reasoning field optional in equipment query schema and improve…
Browse files Browse the repository at this point in the history
… logging for armour suggestions
  • Loading branch information
carloscasalar committed Jan 11, 2025
1 parent f50446c commit 084499d
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 18 deletions.
19 changes: 12 additions & 7 deletions src/PersonalShopper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type SingleSuggestion<SuggestedEntity> = SuggestedEntity | NoSuggestionFo

const queryEquipmentIdsSchema = z.object({
itemIds: z.array(z.string()),
reasoning: z.string(),
reasoning: z.string().optional(),
});

export class PersonalShopper {
Expand All @@ -41,11 +41,15 @@ export class PersonalShopper {

const additionalArmourContext = `suitable for a ${character.role} with ${character.experience} experience.`;
const suitableAmours = await this.equipmentRepository.findByCriteria(armourCriteria, additionalArmourContext, 30);
this.log('Suitable armours:\n', suitableAmours.map((i) => `${i.name} [${i.section}/${i.subsection}] [${i.tl}] [${creditsFromCrFormat(i.price)}] [${i.mass}] [${i.skill}]`));
if (suitableAmours.length === 0) {
return { found: false };
}

const additionalShoppingContext = stripIndent`These are the available items in format "id: name [section/subsection] [tl] [price in credits] [weight in kg] [skill requirement if any]:
${suitableAmours.map((i) => `${i.id}: ${i.name} [${i.section}/${i.subsection}] [${i.tl}] [${i.price}] [${i.mass}] [${i.skill}]`).join('\n')}
${suitableAmours.map((i) => `${i.id}: ${i.name} [${i.section}/${i.subsection}] [${i.tl}] [${creditsFromCrFormat(i.price)}] [${i.mass}] [${i.skill}]`).join('\n')}
`;
this.log('shopping context:', additionalShoppingContext);

const systemMessage = stripIndent`You are a personal shopper for Traveller RPG NPCs.
You will be asked to suggest equipment for a character based on their characteristics, experience, skills and budget.
You will NEVER suggest an item with price higher than the budget.
Expand All @@ -64,13 +68,13 @@ export class PersonalShopper {
.map(([key, value]) => `${key}: ${value}`)
.join(', ')}
I cannot spend more than Cr${budget}.
My budget is ${budget} Credits and I cannot exceed it.
`;
const whatDoIWant = stripIndent`I want you to suggest me one single armour to wear
Answer in JSON format, use the 'reasoning' attribute to explain your choices if you need to:
Answer in JSON format, don't explain the answer:
{
"itemIds": string[],
"reasoning": string
itemIds: string[],
reasoning?: string
}
`;

Expand All @@ -94,6 +98,7 @@ export class PersonalShopper {
}
const armour = suitableAmours.find((a) => a.id === armoursSuggestion.data.itemIds[0]);
if (!armour) {
this.logError(`Armour ID suggested does not exists ${armoursSuggestion.data.itemIds[0]}`);
return { error: 'armour ID suggested does not exists', answer: rawArmourSuggestion };
}

Expand Down
31 changes: 20 additions & 11 deletions src/cloudflare/CloudflareEquipmentRepository.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { creditsFromCrFormat } from './../price';
import { D1Database, VectorizeIndex } from '@cloudflare/workers-types';
import { stripIndents } from 'common-tags';
import { Equipment, EquipmentCriteria, EquipmentRepository } from '../EquipmentRepository';
Expand Down Expand Up @@ -32,19 +33,24 @@ export class CloudflareEquipmentRepository implements EquipmentRepository {
if (criteria.sections.type === 'sections') {
return `section should be one of ${criteria.sections.sections.join(', ')}.`;
}
return `section/subsection should be one of ${criteria.sections.sections.map(({ section, subsection }) => `${section}/${subsection}`).join(', ')}.`;
return `section/subsection should be one of ${criteria.sections.sections
.map(({ section, subsection }) => `${section}/${subsection}`)
.join(', ')}.`;
};
const getPriceFilter = (criteria: EquipmentCriteria) => !criteria.maxPrice ? '' :
`prices should be lower than ${toCrFormat(criteria.maxPrice)} credits. Prices are provided in CrX format where Cr means credits and X is an integer number with comma separator for thousands.`;
const getTLFilter = (criteria: EquipmentCriteria) => !criteria.maxTL ? '' : `TL should be lower than ${criteria.maxTL}.`;
const getAdditionalContext = (additionalContext: string | null) => additionalContext ? `\nAdditional context: ${additionalContext}` : '';
const getPriceFilter = (criteria: EquipmentCriteria) =>
!criteria.maxPrice
? ''
: `prices should be lower than ${criteria.maxPrice} credits. Prices are provided in CrX format where Cr means credits and X is an integer number that can be represented with comma separator for thousands or as a plain integer.`;
const getTLFilter = (criteria: EquipmentCriteria) => (!criteria.maxTL ? '' : `TL should be lower than ${criteria.maxTL}.`);
const getAdditionalContext = (additionalContext: string | null) =>
additionalContext ? `\nAdditional context: ${additionalContext}` : '';
const question = stripIndents`
Suggest equipment items that match the following criteria:
${getSectionsFilter(criteria)}
${getPriceFilter(criteria)}
${getTLFilter(criteria)}
${getAdditionalContext(additionalContext)}
`
`;
this.log('question:', question);

const vectorizedQuery = await this.questionRepository.translateQuestionToEmbeddings(question);
Expand All @@ -54,11 +60,9 @@ export class CloudflareEquipmentRepository implements EquipmentRepository {
return [];
}

const equipmentIds = vectorQuery.matches
.filter((match) => match.score > GOOD_ENOUGH_SCORE_THRESHOLD)
.map((match) => match.id);
const equipmentIds = vectorQuery.matches.filter((match) => match.score > GOOD_ENOUGH_SCORE_THRESHOLD).map((match) => match.id);

if(equipmentIds.length === 0) {
if (equipmentIds.length === 0) {
this.log('no results found on the vectorized results with a good enough score');
return [];
}
Expand All @@ -67,7 +71,12 @@ export class CloudflareEquipmentRepository implements EquipmentRepository {
const allIds = JSON.stringify(equipmentIds);
const { results } = await this.db.prepare(dbQuery).bind(allIds).all<Equipment>();

return results;
const itemsUnderMaxPrice = results
// I couldn't manage the ia to understand this constraint, so I'm forcing it here :(
.filter((equipment) => criteria.maxPrice == undefined || creditsFromCrFormat(equipment.price) <= criteria.maxPrice);
this.log('raw item results:', results.length);
this.log('items under max price:', itemsUnderMaxPrice.length);
return itemsUnderMaxPrice;
}

private log(...args: any[]) {
Expand Down

0 comments on commit 084499d

Please sign in to comment.