diff --git a/plugin-nostr/lib/selfReflection.js b/plugin-nostr/lib/selfReflection.js index eb5efbe..98202f1 100644 --- a/plugin-nostr/lib/selfReflection.js +++ b/plugin-nostr/lib/selfReflection.js @@ -486,7 +486,8 @@ class SelfReflectionEngine { for (const memory of memories) { const data = memory?.content?.data; const analysis = data?.analysis; - if (!analysis) { + // Skip invalid reflections - require proper structure + if (!analysis || !this._hasMinimalReflectionData(analysis)) { continue; } @@ -1291,7 +1292,8 @@ CRITICAL: For each interaction, provide SPECIFIC behavioral changes: - Recommend exact wording alternatives for better engagement - Provide concrete examples of how to restructure responses -OUTPUT JSON ONLY: +IMPORTANT: OUTPUT VALID JSON ONLY - NO MARKDOWN, NO EXPLANATIONS, NO CODE BLOCKS. +Your entire response must be a single valid JSON object with this exact structure: { "strengths": ["Specific successful approaches to continue using"], "weaknesses": ["Exact problematic phrases or patterns to eliminate", "More specific issues"], @@ -1312,16 +1314,120 @@ OUTPUT JSON ONLY: return null; } + // Try extracting JSON first try { const match = response.match(/\{[\s\S]*\}/); - if (!match) { - return null; + if (match) { + const parsed = JSON.parse(match[0]); + if (this._isValidReflection(parsed)) { + return parsed; + } + this.logger.debug('[SELF-REFLECTION] JSON parsed but missing required fields, trying markdown fallback'); } - return JSON.parse(match[0]); } catch (err) { - this.logger.debug('[SELF-REFLECTION] Failed to parse JSON response:', err?.message || err); + this.logger.debug('[SELF-REFLECTION] JSON parse failed, trying markdown fallback:', err?.message || err); + } + + // Fallback: extract fields from markdown response + const markdownExtracted = this._extractFieldsFromMarkdown(response); + if (markdownExtracted && this._hasMinimalReflectionData(markdownExtracted)) { + this.logger.debug('[SELF-REFLECTION] Extracted reflection from markdown response'); + return markdownExtracted; + } + + this.logger.debug('[SELF-REFLECTION] Failed to extract valid reflection from response'); + return null; + } + + _extractFieldsFromMarkdown(text) { + if (!text || typeof text !== 'string') { return null; } + + const result = { + strengths: [], + weaknesses: [], + patterns: [], + recommendations: [], + exampleGoodReply: null, + exampleBadReply: null, + regressions: [], + improvements: [] + }; + + // Extract list items following headers or labels + // Limit input to prevent ReDoS on large responses + const limitedText = text.length > 10000 ? text.slice(0, 10000) : text; + const extractListItems = (pattern) => { + const matches = []; + const regex = new RegExp(pattern + '[:\\s]*([^\\n]+(?:\\n[-*•]\\s*[^\\n]+)*)', 'gi'); + const match = limitedText.match(regex); + if (match) { + for (const m of match) { + // Extract bullet points + const bullets = m.match(/[-*•]\s*([^\n]+)/g); + if (bullets) { + for (const bullet of bullets) { + const cleaned = bullet.replace(/^[-*•]\s*/, '').trim(); + if (cleaned && cleaned.length > 3) { + matches.push(cleaned); + } + } + } + // Also extract non-bullet content after the header + const headerMatch = m.match(new RegExp(pattern + '[:\\s]*([^\\n]+)', 'i')); + if (headerMatch && headerMatch[1] && !headerMatch[1].match(/^[-*•]/)) { + const items = headerMatch[1].split(/[;,]/).map(s => s.trim()).filter(s => s.length > 3); + matches.push(...items); + } + } + } + return matches; + }; + + // Extract each field type + // Note: Explicitly match "you're" or "youre" to prevent "your" from matching + result.strengths = extractListItems('(?:strengths?|what(?:\'s| is| (?:you\'re|youre)) (?:working|doing) well|positives?)'); + result.weaknesses = extractListItems('(?:weaknesses?|what needs? (?:improvement|work)|areas? (?:to|for) improv|negatives?|issues?)'); + result.patterns = extractListItems('(?:patterns?|repeated behaviors?|habits?)'); + result.recommendations = extractListItems('(?:recommendations?|suggestions?|actionable (?:changes?|improvements?)|next steps?|advice)'); + result.regressions = extractListItems('(?:regressions?|(?:where|areas?) (?:you )?slipped|went backwards?)'); + result.improvements = extractListItems('(?:improvements?|(?:where|areas?) (?:you )?improved|progress)'); + + // Extract quoted examples + const goodReplyMatch = text.match(/(?:best|good|strong|example good)\s*(?:reply|response|moment)[:\s]*["\u201c]([^"\u201d]+)["\u201d]/i); + if (goodReplyMatch) { + result.exampleGoodReply = goodReplyMatch[1].trim(); + } + + const badReplyMatch = text.match(/(?:worst|bad|weak|example bad)\s*(?:reply|response|moment)[:\s]*["\u201c]([^"\u201d]+)["\u201d]/i); + if (badReplyMatch) { + result.exampleBadReply = badReplyMatch[1].trim(); + } + + return result; + } + + _isValidReflection(analysis) { + if (!analysis || typeof analysis !== 'object') { + return false; + } + // Require at least strengths, weaknesses, and recommendations to be arrays + return ( + Array.isArray(analysis.strengths) && + Array.isArray(analysis.weaknesses) && + Array.isArray(analysis.recommendations) + ); + } + + _hasMinimalReflectionData(analysis) { + if (!analysis || typeof analysis !== 'object') { + return false; + } + // For markdown extraction, be more lenient - require at least 2 fields with data + const fieldsWithData = ['strengths', 'weaknesses', 'patterns', 'recommendations', 'regressions', 'improvements'] + .filter(field => Array.isArray(analysis[field]) && analysis[field].length > 0); + return fieldsWithData.length >= 2; } _createUuid(seed) { diff --git a/plugin-nostr/test/selfReflection.extraction.test.js b/plugin-nostr/test/selfReflection.extraction.test.js new file mode 100644 index 0000000..63c11a1 --- /dev/null +++ b/plugin-nostr/test/selfReflection.extraction.test.js @@ -0,0 +1,342 @@ +const { SelfReflectionEngine } = require('../lib/selfReflection'); + +describe('SelfReflectionEngine JSON and Markdown Extraction', () => { + let engine; + const runtime = { + getSetting: () => null + }; + + beforeEach(() => { + engine = new SelfReflectionEngine(runtime, console, {}); + }); + + describe('_extractJson with valid JSON', () => { + it('extracts valid JSON with all required fields', () => { + const response = `{ + "strengths": ["Good engagement", "Witty replies"], + "weaknesses": ["Too verbose", "Overusing emojis"], + "patterns": ["Starting with questions"], + "recommendations": ["Be more concise"], + "exampleGoodReply": "That's a great point!", + "exampleBadReply": "Well, you see, I think that maybe perhaps...", + "regressions": ["Less concise than before"], + "improvements": ["Better topic awareness"] + }`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toEqual(["Good engagement", "Witty replies"]); + expect(result.weaknesses).toEqual(["Too verbose", "Overusing emojis"]); + expect(result.recommendations).toEqual(["Be more concise"]); + }); + + it('extracts JSON embedded in text', () => { + const response = `Here is my analysis: + + { + "strengths": ["Concise replies"], + "weaknesses": ["Missing context"], + "recommendations": ["Add more detail"], + "patterns": [] + } + + That's my reflection.`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toEqual(["Concise replies"]); + }); + + it('returns null for JSON missing required fields', () => { + // This should fail validation and try markdown fallback + const response = `{ + "thoughts": "Just some random thoughts", + "summary": "A summary" + }`; + + const result = engine._extractJson(response); + expect(result).toBeNull(); + }); + }); + + describe('_extractJson with markdown fallback', () => { + it('extracts from markdown with headers and bullet points', () => { + const response = `## Self-Reflection Analysis + +### Strengths: +- Good engagement with users +- Witty and memorable replies +- Consistent personality + +### Weaknesses: +- Sometimes too verbose +- Overusing certain phrases + +### Recommendations: +- Be more concise in responses +- Vary sentence structures`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toContain('Good engagement with users'); + expect(result.strengths).toContain('Witty and memorable replies'); + expect(result.weaknesses).toContain('Sometimes too verbose'); + expect(result.recommendations).toContain('Be more concise in responses'); + }); + + it('extracts from markdown with different label formats', () => { + const response = `**What you're doing well:** +- Engaging authentically +- Quick response time + +**What needs improvement:** +- Could be more concise +- Missing follow-up questions + +**Actionable changes:** +- Trim responses by 30% +- Ask clarifying questions`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths.length).toBeGreaterThan(0); + expect(result.weaknesses.length).toBeGreaterThan(0); + expect(result.recommendations.length).toBeGreaterThan(0); + }); + + it('extracts quoted examples from markdown', () => { + const response = `## Analysis + +Strengths: +- Good tone + +Weaknesses: +- Too long + +Best reply: "Short and sweet" +Worst reply: "Well, actually, if you think about it from multiple perspectives..." + +Recommendations: +- Keep it brief`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.exampleGoodReply).toBe('Short and sweet'); + expect(result.exampleBadReply).toContain('Well, actually'); + }); + + it('extracts improvements and regressions', () => { + const response = `## Reflection + +Strengths: +- Better focus + +Weaknesses: +- Still verbose + +Areas where you improved: +- Topic awareness +- Response timing + +Where you slipped: +- Conciseness +- Emoji overuse + +Recommendations: +- Continue improving`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.improvements.length).toBeGreaterThan(0); + expect(result.regressions.length).toBeGreaterThan(0); + }); + + it('returns null when markdown has insufficient data', () => { + const response = `This is just a general comment about the conversation. + Nothing specific to extract here. + Maybe some thoughts.`; + + const result = engine._extractJson(response); + expect(result).toBeNull(); + }); + }); + + describe('_isValidReflection', () => { + it('returns true for valid reflection structure', () => { + const analysis = { + strengths: ['Good'], + weaknesses: ['Bad'], + recommendations: ['Do better'] + }; + expect(engine._isValidReflection(analysis)).toBe(true); + }); + + it('returns false when missing required arrays', () => { + expect(engine._isValidReflection({ strengths: ['Good'] })).toBe(false); + expect(engine._isValidReflection({ strengths: 'not array', weaknesses: [], recommendations: [] })).toBe(false); + expect(engine._isValidReflection(null)).toBe(false); + expect(engine._isValidReflection({})).toBe(false); + }); + }); + + describe('_hasMinimalReflectionData', () => { + it('returns true when at least 2 fields have data', () => { + expect(engine._hasMinimalReflectionData({ + strengths: ['Good'], + weaknesses: ['Bad'], + recommendations: [], + patterns: [] + })).toBe(true); + }); + + it('returns false when less than 2 fields have data', () => { + expect(engine._hasMinimalReflectionData({ + strengths: ['Good'], + weaknesses: [], + recommendations: [], + patterns: [] + })).toBe(false); + }); + + it('returns false for null or non-object', () => { + expect(engine._hasMinimalReflectionData(null)).toBe(false); + expect(engine._hasMinimalReflectionData('string')).toBe(false); + }); + }); + + describe('_extractFieldsFromMarkdown edge cases', () => { + it('handles asterisk bullets', () => { + const text = `Strengths: +* First strength +* Second strength`; + + const result = engine._extractFieldsFromMarkdown(text); + expect(result.strengths).toContain('First strength'); + expect(result.strengths).toContain('Second strength'); + }); + + it('handles unicode bullets', () => { + const text = `Weaknesses: +• First issue +• Second issue`; + + const result = engine._extractFieldsFromMarkdown(text); + expect(result.weaknesses).toContain('First issue'); + expect(result.weaknesses).toContain('Second issue'); + }); + + it('handles semicolon-separated items', () => { + const text = `Patterns: using "well"; starting with questions; emoji overuse`; + + const result = engine._extractFieldsFromMarkdown(text); + // Should extract items after header + expect(result.patterns.length).toBeGreaterThan(0); + }); + + it('returns empty arrays for missing sections', () => { + const text = `Just some random text without any sections`; + + const result = engine._extractFieldsFromMarkdown(text); + expect(result.strengths).toEqual([]); + expect(result.weaknesses).toEqual([]); + expect(result.recommendations).toEqual([]); + }); + + it('handles null/undefined/empty input', () => { + expect(engine._extractFieldsFromMarkdown(null)).toBeNull(); + expect(engine._extractFieldsFromMarkdown(undefined)).toBeNull(); + // Empty string is technically a valid string, but guard treats it as falsy + expect(engine._extractFieldsFromMarkdown('')).toBeNull(); + }); + + it('filters out very short items (length <= 3)', () => { + const text = `Strengths: +- OK +- A valid strength item +- Yes +- No`; + + const result = engine._extractFieldsFromMarkdown(text); + expect(result.strengths).toEqual(['A valid strength item']); + expect(result.strengths).not.toContain('OK'); + expect(result.strengths).not.toContain('Yes'); + expect(result.strengths).not.toContain('No'); + }); + + it('does not match "your" as "you\'re"', () => { + const text = `What your doing well: +- This should NOT match as strengths + +Strengths: +- This should match`; + + const result = engine._extractFieldsFromMarkdown(text); + // "your" should not match the you're pattern + expect(result.strengths).not.toContain('This should NOT match as strengths'); + expect(result.strengths).toContain('This should match'); + }); + }); + + describe('integration: mixed JSON and markdown scenarios', () => { + it('prefers valid JSON over markdown', () => { + const response = `## Analysis + +Strengths: +- From markdown + +{ + "strengths": ["From JSON"], + "weaknesses": ["From JSON"], + "recommendations": ["From JSON"] +}`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toEqual(["From JSON"]); + }); + + it('falls back to markdown when JSON is malformed', () => { + const response = `## Analysis + +{ + "strengths": ["Valid array", + // oops, bad JSON +} + +Strengths: +- Fallback strength +- Another strength + +Weaknesses: +- Fallback weakness + +Recommendations: +- Fallback recommendation`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toContain('Fallback strength'); + }); + + it('falls back to markdown when JSON missing required fields', () => { + const response = `{ + "notes": "This is not a valid reflection format" +} + +But here's the real analysis: + +Strengths: +- Actual strength + +Weaknesses: +- Actual weakness + +Recommendations: +- Actual recommendation`; + + const result = engine._extractJson(response); + expect(result).not.toBeNull(); + expect(result.strengths).toContain('Actual strength'); + }); + }); +});