From 8211ce0d08a34b198be445b72648ecd27036b947 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 28 Apr 2026 13:52:13 -0700 Subject: [PATCH] Add prompt playbook assembler --- prompts/playbooks/README.md | 7 + prompts/playbooks/buyer-car-dealership.md | 17 +++ prompts/playbooks/buyer-car-private-seller.md | 17 +++ prompts/playbooks/core-negotiation-agent.md | 19 +++ prompts/playbooks/seller-marketplace.md | 18 +++ src/lib/prompts/promptAssembler.js | 130 ++++++++++++++++++ src/lib/prompts/promptAssembler.test.js | 90 ++++++++++++ 7 files changed, 298 insertions(+) create mode 100644 prompts/playbooks/README.md create mode 100644 prompts/playbooks/buyer-car-dealership.md create mode 100644 prompts/playbooks/buyer-car-private-seller.md create mode 100644 prompts/playbooks/core-negotiation-agent.md create mode 100644 prompts/playbooks/seller-marketplace.md create mode 100644 src/lib/prompts/promptAssembler.js create mode 100644 src/lib/prompts/promptAssembler.test.js diff --git a/prompts/playbooks/README.md b/prompts/playbooks/README.md new file mode 100644 index 0000000..05332c0 --- /dev/null +++ b/prompts/playbooks/README.md @@ -0,0 +1,7 @@ +# Haggly Prompt Playbooks + +These files are concise source material for the v2 Haggly agent harness. + +They are not canned scripts. The API prompt assembler selects the relevant playbooks, combines them with structured negotiation context, and lets the hosted model decide whether to ask questions, recommend a move, or draft wording. + +Original research and older car negotiation notes remain in `research/` and `scripts/` as reference material. diff --git a/prompts/playbooks/buyer-car-dealership.md b/prompts/playbooks/buyer-car-dealership.md new file mode 100644 index 0000000..7482573 --- /dev/null +++ b/prompts/playbooks/buyer-car-dealership.md @@ -0,0 +1,17 @@ +## Buyer Car Dealership + +Use this when the user is buying a vehicle from a dealership. + +Buyer priorities: + +- Focus on out-the-door price, not monthly payment. +- Keep financing, trade-in, and vehicle price separate until the user chooses otherwise. +- Watch for urgency pressure and manager-check loops. +- Compare the deal against market value and the user's walk-away number. + +Useful dealership moves: + +- Ask for the full out-the-door price in writing. +- Slow down same-day pressure. +- Treat dealer add-ons and fees as negotiable unless legally required. +- Recommend leaving when the dealer will not clarify total cost. diff --git a/prompts/playbooks/buyer-car-private-seller.md b/prompts/playbooks/buyer-car-private-seller.md new file mode 100644 index 0000000..29e8d4e --- /dev/null +++ b/prompts/playbooks/buyer-car-private-seller.md @@ -0,0 +1,17 @@ +## Buyer Car Private Seller + +Use this when the user is buying a vehicle from an individual owner or private seller. + +Buyer priorities: + +- Build enough rapport to keep the seller talking. +- Gather condition, maintenance, ownership, and urgency details before offering. +- Use discovered concerns as the reason for price movement. +- Keep the offer calm and tied to specific facts. + +Useful private-seller moves: + +- Ask about maintenance records, accidents, tires, brakes, title status, and selling reason. +- If concerns appear, name two or three facts and make a grounded offer. +- If the seller is emotionally attached, acknowledge care before discussing price. +- If the seller has urgency, use speed and certainty as leverage. diff --git a/prompts/playbooks/core-negotiation-agent.md b/prompts/playbooks/core-negotiation-agent.md new file mode 100644 index 0000000..63f0eb4 --- /dev/null +++ b/prompts/playbooks/core-negotiation-agent.md @@ -0,0 +1,19 @@ +## Core Negotiation Agent + +Haggly is an agentic negotiation assistant. Diagnose before drafting. + +Operating rules: + +- Read the leverage before recommending wording. +- Identify missing context instead of guessing. +- Ask one useful follow-up question when the next move is unclear. +- Recommend the next move before writing a message. +- Draft wording only when drafting is the right next step. +- Keep advice practical, calm, and specific to the user's side of the negotiation. + +Response shape: + +1. Current read +2. Missing context +3. Next move +4. Suggested wording, only if useful diff --git a/prompts/playbooks/seller-marketplace.md b/prompts/playbooks/seller-marketplace.md new file mode 100644 index 0000000..f2e7cbe --- /dev/null +++ b/prompts/playbooks/seller-marketplace.md @@ -0,0 +1,18 @@ +## Seller Marketplace + +Use this when the user is selling an item in a marketplace-style negotiation. + +Seller priorities: + +- Protect the floor price. +- Avoid over-explaining. +- Test buyer seriousness before making concessions. +- Tie concessions to clean logistics, such as pickup timing or payment certainty. +- If the offer meets the seller's minimum, move toward logistics instead of reopening price. + +Useful seller moves: + +- For lowball offers, acknowledge once and redirect to a serious range. +- For close offers, counter lightly or accept with clear pickup terms. +- For acceptable offers, confirm the deal and move to scheduling. +- If the buyer points out flaws, avoid defensiveness and restate how pricing already accounts for condition. diff --git a/src/lib/prompts/promptAssembler.js b/src/lib/prompts/promptAssembler.js new file mode 100644 index 0000000..d4798b6 --- /dev/null +++ b/src/lib/prompts/promptAssembler.js @@ -0,0 +1,130 @@ +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..') + +const PLAYBOOKS = [ + { + id: 'core-negotiation-agent', + path: 'prompts/playbooks/core-negotiation-agent.md', + }, + { + id: 'seller-marketplace', + path: 'prompts/playbooks/seller-marketplace.md', + }, + { + id: 'buyer-car-dealership', + path: 'prompts/playbooks/buyer-car-dealership.md', + }, + { + id: 'buyer-car-private-seller', + path: 'prompts/playbooks/buyer-car-private-seller.md', + }, +] + +const playbookCache = new Map() + +function getPlaybookById(id) { + const playbook = PLAYBOOKS.find((candidate) => candidate.id === id) + + if (!playbook) { + throw new Error(`Unknown playbook: ${id}`) + } + + if (!playbookCache.has(id)) { + playbookCache.set(id, { + ...playbook, + content: readFileSync(resolve(repoRoot, playbook.path), 'utf8').trim(), + }) + } + + return playbookCache.get(id) +} + +function includesAny(text, terms) { + return terms.some((term) => text.includes(term)) +} + +function getSelectionText({ context, latestMessage }) { + return `${context?.item ?? ''} ${latestMessage ?? ''}`.toLowerCase() +} + +function getBuyerPlaybookId(selectionText) { + const isDealership = includesAny(selectionText, [ + 'dealer', + 'dealership', + 'out-the-door', + 'otd', + 'manager', + 'financing', + ]) + + if (isDealership) { + return 'buyer-car-dealership' + } + + const isPrivateCarSale = includesAny(selectionText, [ + 'car', + 'vehicle', + 'truck', + 'honda', + 'civic', + 'private seller', + 'owner', + ]) + + if (isPrivateCarSale) { + return 'buyer-car-private-seller' + } + + return null +} + +function formatConversation(conversation) { + if (!conversation?.length) { + return '(none)' + } + + return conversation + .map((message) => `${message.role}: ${message.content}`) + .join('\n') +} + +export function selectPlaybooks({ context = {}, latestMessage = '' } = {}) { + const selectedIds = ['core-negotiation-agent'] + + if (context.mode === 'buyer') { + const buyerPlaybookId = getBuyerPlaybookId(getSelectionText({ context, latestMessage })) + + if (buyerPlaybookId) { + selectedIds.push(buyerPlaybookId) + } + } else { + selectedIds.push('seller-marketplace') + } + + return selectedIds.map(getPlaybookById) +} + +export function assembleHagglyPrompt({ context = {}, latestMessage = '', conversation = [] } = {}) { + const playbooks = selectPlaybooks({ context, latestMessage }) + + return { + playbookIds: playbooks.map((playbook) => playbook.id), + system: [ + 'You are Haggly, an agentic AI negotiation assistant.', + 'Do not behave like a script generator. Diagnose first, ask for missing context when needed, and draft only when drafting is the right next move.', + 'Use these playbooks as guidance, not as canned copy.', + playbooks.map((playbook) => playbook.content).join('\n\n'), + ].join('\n\n'), + user: [ + 'Negotiation context JSON:', + JSON.stringify(context, null, 2), + 'Latest user message:', + latestMessage || '(none)', + 'Conversation history:', + formatConversation(conversation), + ].join('\n\n'), + } +} diff --git a/src/lib/prompts/promptAssembler.test.js b/src/lib/prompts/promptAssembler.test.js new file mode 100644 index 0000000..d5ddf52 --- /dev/null +++ b/src/lib/prompts/promptAssembler.test.js @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' +import { buildNegotiationContext } from '../../features/negotiation/negotiationContext' +import { assembleHagglyPrompt, selectPlaybooks } from './promptAssembler' + +describe('selectPlaybooks', () => { + it('selects core and seller marketplace guidance for seller context', () => { + const context = buildNegotiationContext({ + mode: 'seller', + item: 'gaming chair', + askingPrice: 200, + theirOffer: 80, + }) + + const playbooks = selectPlaybooks({ + context, + latestMessage: 'They said they can pick up today if I take $80.', + }) + + expect(playbooks.map((playbook) => playbook.id)).toEqual([ + 'core-negotiation-agent', + 'seller-marketplace', + ]) + }) + + it('selects dealership guidance for a buyer working with a dealer', () => { + const context = buildNegotiationContext({ + mode: 'buyer', + item: 'used Honda Civic', + askingPrice: 18000, + }) + + const playbooks = selectPlaybooks({ + context, + latestMessage: 'I am buying from a dealership and want the out-the-door price.', + }) + + expect(playbooks.map((playbook) => playbook.id)).toEqual([ + 'core-negotiation-agent', + 'buyer-car-dealership', + ]) + }) + + it('selects private-seller car guidance for a buyer negotiating with an owner', () => { + const context = buildNegotiationContext({ + mode: 'buyer', + item: 'used truck', + askingPrice: 22000, + }) + + const playbooks = selectPlaybooks({ + context, + latestMessage: 'This is a private seller listing from the owner.', + }) + + expect(playbooks.map((playbook) => playbook.id)).toEqual([ + 'core-negotiation-agent', + 'buyer-car-private-seller', + ]) + }) +}) + +describe('assembleHagglyPrompt', () => { + it('assembles a prompt with playbooks and structured context without an API key', () => { + const context = buildNegotiationContext({ + mode: 'seller', + item: 'gaming chair', + askingPrice: 200, + theirOffer: 80, + minimumPrice: 160, + }) + + const prompt = assembleHagglyPrompt({ + context, + latestMessage: 'They offered $80 and want to pick up tonight.', + conversation: [ + { role: 'user', content: 'I am selling a gaming chair for $200.' }, + { role: 'assistant', content: 'What is your minimum?' }, + ], + }) + + expect(prompt.playbookIds).toEqual(['core-negotiation-agent', 'seller-marketplace']) + expect(prompt.system).toContain('You are Haggly') + expect(prompt.system).toContain('Do not behave like a script generator') + expect(prompt.system).toContain('## Core Negotiation Agent') + expect(prompt.system).toContain('## Seller Marketplace') + expect(prompt.user).toContain('"offerType": "lowball"') + expect(prompt.user).toContain('They offered $80 and want to pick up tonight.') + expect(prompt.user).toContain('What is your minimum?') + }) +})