From 442c1db5808af67537a31b5afc3e3695eb8db5cc Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Wed, 17 Dec 2025 14:56:39 +0100 Subject: [PATCH] Hunter Companion: fix hallucination (strict no-invent rules), better text formatting with spacing --- backend/app/services/llm_agent.py | 54 +++++++------ .../src/components/chat/HunterCompanion.tsx | 75 ++++++++++++------- 2 files changed, 74 insertions(+), 55 deletions(-) diff --git a/backend/app/services/llm_agent.py b/backend/app/services/llm_agent.py index 9999a30..7e12ff5 100644 --- a/backend/app/services/llm_agent.py +++ b/backend/app/services/llm_agent.py @@ -63,23 +63,26 @@ async def _get_user_tier(db: AsyncSession, user: User) -> str: def _build_system_prompt(path: str) -> str: tools = tool_catalog_for_prompt(path) return ( - "You are the Pounce Hunter Companion, an expert domain trading assistant. Always respond in English.\n" - "You help users with: domain analysis, auction hunting, portfolio management, and trading decisions.\n\n" - "RESPONSE FORMAT (CRITICAL):\n" - "- Write in plain text. NO markdown asterisks, NO ** or *, NO code blocks.\n" - "- Use simple dashes (-) for bullet points.\n" - "- Keep responses concise: 2-4 sentences intro, then bullets if needed.\n" - "- Never show tool outputs, JSON, or internal data to the user.\n\n" - "BEHAVIOR:\n" - "- Be helpful, direct, and conversational like a knowledgeable colleague.\n" - "- For domain questions: give a clear BUY / CONSIDER / SKIP recommendation with 3-5 reasons.\n" - "- Do NOT invent user preferences, keywords, or data. Ask if unclear.\n" - "- For greetings: respond naturally and ask how you can help.\n\n" + "You are the Pounce Hunter Companion, a domain trading expert. Always respond in English.\n\n" + "CRITICAL RULES:\n" + "1. NEVER invent or hallucinate data. You do NOT have access to SEMrush, Estibot, GoDaddy sales, or external databases.\n" + "2. If you don't have data, say so honestly. Only use data from tools you actually called.\n" + "3. Keep responses SHORT: 2-3 sentences max, then bullets if needed.\n" + "4. NO markdown: no ** or *, no code blocks, no headers with #.\n" + "5. Use dashes (-) for bullet points.\n\n" + "WHAT YOU CAN DO:\n" + "- Analyze domains using the analyze_domain tool (gives Pounce Score, risk, value estimate)\n" + "- Show user's watchlist, portfolio, listings, inbox, yield data\n" + "- Search auctions and drops\n" + "- Generate brandable names\n\n" + "WHAT YOU CANNOT DO:\n" + "- Access external sales databases or SEO tools\n" + "- Look up real-time WHOIS or DNS (unless via tool)\n" + "- Make up sales history or traffic stats\n\n" "TOOL USAGE:\n" - "- Use tools when user asks about their data (watchlist, portfolio, listings, inbox, yield) or a specific domain.\n" - "- To call tools, respond with ONLY: {\"tool_calls\":[{\"name\":\"...\",\"args\":{...}}]}\n" - "- After receiving tool results, answer naturally without mentioning tools.\n\n" - f"AVAILABLE TOOLS:\n{json.dumps(tools, ensure_ascii=False)}\n" + "- To call a tool, respond with ONLY: {\"tool_calls\":[{\"name\":\"...\",\"args\":{...}}]}\n" + "- After tool results, summarize briefly without mentioning tools.\n\n" + f"TOOLS:\n{json.dumps(tools, ensure_ascii=False)}\n" ) @@ -143,13 +146,8 @@ async def run_agent( { "role": "assistant", "content": ( - "Hey! How can I help you today?\n\n" - "I can help with:\n" - "- Analyzing a specific domain\n" - "- Finding auction deals or drops\n" - "- Reviewing your portfolio or watchlist\n" - "- Checking your listings and leads\n\n" - "Just tell me what you need." + "Hey! What can I help you with?\n\n" + "Give me a domain to analyze, or ask about your watchlist, portfolio, or current auctions." ), } ) @@ -206,11 +204,11 @@ async def stream_final_answer(convo: list[dict[str, Any]], *, model: Optional[st { "role": "system", "content": ( - "Final step: respond to the user in plain text.\n" - "- NO markdown: no ** or * for bold/italic, no code blocks.\n" - "- Use dashes (-) for bullets.\n" - "- Do NOT output JSON or mention tools.\n" - "- Be concise and helpful." + "Respond now. Rules:\n" + "- NEVER invent data. Only use data from tools you called.\n" + "- Keep it SHORT: 2-3 sentences, then bullet points if needed.\n" + "- NO markdown (no ** or *), just plain text with dashes for bullets.\n" + "- Do NOT mention tools or JSON." ), } ], diff --git a/frontend/src/components/chat/HunterCompanion.tsx b/frontend/src/components/chat/HunterCompanion.tsx index f47b410..976ceed 100644 --- a/frontend/src/components/chat/HunterCompanion.tsx +++ b/frontend/src/components/chat/HunterCompanion.tsx @@ -98,27 +98,52 @@ function getTier(subscription: any): 'scout' | 'trader' | 'tycoon' { return 'scout' } -// Simple markdown-like formatting to clean HTML +// Format message text to clean HTML with proper spacing function formatMessage(text: string): string { if (!text) return '' + + // Escape HTML first let html = text - // Escape HTML .replace(/&/g, '&') .replace(//g, '>') - // Bold: **text** or __text__ - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/__(.+?)__/g, '$1') - // Italic: *text* or _text_ (but not inside words) - .replace(/(?$1') - .replace(/(?$1') - // Inline code: `code` - .replace(/`([^`]+)`/g, '$1') - // Line breaks - .replace(/\n/g, '
') - // Bullet points: - item or • item - .replace(/
[-•]\s+/g, '
• ') - return html + + // Remove markdown formatting (** and * for bold/italic) + html = html + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/__(.+?)__/g, '$1') + .replace(/_([^_]+)_/g, '$1') + + // Split into paragraphs (double newline = paragraph break) + const paragraphs = html.split(/\n\n+/) + + const formatted = paragraphs.map(para => { + // Check if this paragraph is a list (starts with - or number.) + const lines = para.split('\n') + const isList = lines.every(line => { + const trimmed = line.trim() + return trimmed === '' || trimmed.startsWith('-') || trimmed.startsWith('•') || /^\d+\./.test(trimmed) + }) + + if (isList) { + // Format as list + const items = lines + .map(line => line.trim()) + .filter(line => line) + .map(line => { + // Remove leading dash, bullet, or number + const content = line.replace(/^[-•]\s*/, '').replace(/^\d+\.\s*/, '') + return `
${content}
` + }) + return `
${items.join('')}
` + } else { + // Regular paragraph - convert single newlines to line breaks + return `

${para.replace(/\n/g, '
')}

` + } + }) + + return formatted.join('') } // Suggestion chips based on current page @@ -514,21 +539,17 @@ export function HunterCompanion() { .prose-chat { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; - line-height: 1.6; + line-height: 1.7; } - .prose-chat strong { - color: rgba(255, 255, 255, 0.95); - font-weight: 600; + .prose-chat p { + margin-bottom: 0.75rem; } - .prose-chat em { - color: rgba(255, 255, 255, 0.7); - font-style: italic; + .prose-chat p:last-child { + margin-bottom: 0; } - .prose-chat code { - background: rgba(255, 255, 255, 0.08); - padding: 1px 4px; - border-radius: 2px; - font-size: 11px; + .prose-chat .space-y-0\.5 > div { + padding-top: 0.125rem; + padding-bottom: 0.125rem; } `}