mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
@@ -8,7 +8,8 @@
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@lobechat/memory-user-memory": "workspace:*"
|
||||
"@lobechat/memory-user-memory": "workspace:*",
|
||||
"@lobechat/prompts": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lobechat/types": "workspace:*"
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
RemoveIdentityActionSchema,
|
||||
UpdateIdentityActionSchema,
|
||||
} from '@lobechat/memory-user-memory/schemas';
|
||||
import { formatMemorySearchResults } from '@lobechat/prompts';
|
||||
import { BaseExecutor, type BuiltinToolResult, SearchMemoryParams } from '@lobechat/types';
|
||||
import type { z } from 'zod';
|
||||
|
||||
@@ -14,35 +15,6 @@ import { userMemoryService } from '@/services/userMemory';
|
||||
import { MemoryIdentifier } from '../manifest';
|
||||
import { MemoryApiName } from '../types';
|
||||
|
||||
/**
|
||||
* Format search results into human-readable summary
|
||||
*/
|
||||
const formatSearchResultsSummary = (result: {
|
||||
contexts: unknown[];
|
||||
experiences: unknown[];
|
||||
preferences: unknown[];
|
||||
}): string => {
|
||||
const total = result.contexts.length + result.experiences.length + result.preferences.length;
|
||||
|
||||
if (total === 0) {
|
||||
return '🔍 No memories found matching the query.';
|
||||
}
|
||||
|
||||
const parts: string[] = [`🔍 Found ${total} memories:`];
|
||||
|
||||
if (result.contexts.length > 0) {
|
||||
parts.push(`- ${result.contexts.length} context memories`);
|
||||
}
|
||||
if (result.experiences.length > 0) {
|
||||
parts.push(`- ${result.experiences.length} experience memories`);
|
||||
}
|
||||
if (result.preferences.length > 0) {
|
||||
parts.push(`- ${result.preferences.length} preference memories`);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Memory Tool Executor
|
||||
*
|
||||
@@ -62,7 +34,7 @@ class MemoryExecutor extends BaseExecutor<typeof MemoryApiName> {
|
||||
const result = await userMemoryService.searchMemory(params);
|
||||
|
||||
return {
|
||||
content: formatSearchResultsSummary(result),
|
||||
content: formatMemorySearchResults({ query: params.query, results: result }),
|
||||
state: result,
|
||||
success: true,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`formatMemorySearchResults > should format context memories with full content 1`] = `
|
||||
"<memories query="project" total="1">
|
||||
<contexts count="1">
|
||||
<context id="ctx-1" title="Web App Project" urgency=7 impact=8 type="project" status="in-progress">
|
||||
Building a new web application
|
||||
<subjects>John (person)</subjects>
|
||||
<objects>React (application)</objects>
|
||||
</context>
|
||||
</contexts>
|
||||
</memories>"
|
||||
`;
|
||||
|
||||
exports[`formatMemorySearchResults > should format experience memories with full content 1`] = `
|
||||
"<memories query="debugging" total="1">
|
||||
<experiences count="1">
|
||||
<experience id="exp-1" type="lesson" confidence=9>
|
||||
<situation>Debugging complex state issues</situation>
|
||||
<keyLearning>Breakpoints save time in complex debugging</keyLearning>
|
||||
</experience>
|
||||
</experiences>
|
||||
</memories>"
|
||||
`;
|
||||
|
||||
exports[`formatMemorySearchResults > should format mixed results with all memory types 1`] = `
|
||||
"<memories query="work" total="3">
|
||||
<contexts count="1">
|
||||
<context id="ctx-1" title="Context Title">
|
||||
Context description
|
||||
</context>
|
||||
</contexts>
|
||||
<experiences count="1">
|
||||
<experience id="exp-1">
|
||||
<situation>Situation</situation>
|
||||
<keyLearning>Key learning</keyLearning>
|
||||
</experience>
|
||||
</experiences>
|
||||
<preferences count="1">
|
||||
<preference id="pref-1">Directive</preference>
|
||||
</preferences>
|
||||
</memories>"
|
||||
`;
|
||||
|
||||
exports[`formatMemorySearchResults > should format preference memories with full content 1`] = `
|
||||
"<memories query="code style" total="1">
|
||||
<preferences count="1">
|
||||
<preference id="pref-1" type="coding-standard" priority=10>Always use TypeScript strict mode</preference>
|
||||
</preferences>
|
||||
</memories>"
|
||||
`;
|
||||
|
||||
exports[`formatMemorySearchResults > should handle null and undefined values gracefully 1`] = `
|
||||
"<memories query="test" total="1">
|
||||
<contexts count="1">
|
||||
<context id="ctx-1"></context>
|
||||
</contexts>
|
||||
</memories>"
|
||||
`;
|
||||
|
||||
exports[`formatMemorySearchResults > should return empty results message when no memories found 1`] = `
|
||||
"<memories query="test query">
|
||||
<status>No memories found matching the query.</status>
|
||||
</memories>"
|
||||
`;
|
||||
@@ -0,0 +1,200 @@
|
||||
import { UserMemoryContextObjectType, UserMemoryContextSubjectType } from '@lobechat/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatMemorySearchResults } from './formatSearchResults';
|
||||
|
||||
describe('formatMemorySearchResults', () => {
|
||||
it('should return empty results message when no memories found', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'test query',
|
||||
results: {
|
||||
contexts: [],
|
||||
experiences: [],
|
||||
preferences: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format context memories with full content', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'project',
|
||||
results: {
|
||||
contexts: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
associatedObjects: [{ name: 'React', type: UserMemoryContextObjectType.Application }],
|
||||
associatedSubjects: [{ name: 'John', type: UserMemoryContextSubjectType.Person }],
|
||||
createdAt: new Date('2024-01-01'),
|
||||
currentStatus: 'in-progress',
|
||||
description: 'Building a new web application',
|
||||
id: 'ctx-1',
|
||||
metadata: null,
|
||||
scoreImpact: 8,
|
||||
scoreUrgency: 7,
|
||||
tags: ['frontend', 'react'],
|
||||
title: 'Web App Project',
|
||||
type: 'project',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryIds: null,
|
||||
},
|
||||
],
|
||||
experiences: [],
|
||||
preferences: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format experience memories with full content', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'debugging',
|
||||
results: {
|
||||
contexts: [],
|
||||
experiences: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
action: 'Used breakpoints instead of console.log',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
id: 'exp-1',
|
||||
keyLearning: 'Breakpoints save time in complex debugging',
|
||||
metadata: null,
|
||||
possibleOutcome: 'Faster debugging sessions',
|
||||
reasoning: 'Console logs clutter the code',
|
||||
scoreConfidence: 9,
|
||||
situation: 'Debugging complex state issues',
|
||||
tags: ['debugging', 'best-practice'],
|
||||
type: 'lesson',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryId: null,
|
||||
},
|
||||
],
|
||||
preferences: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format preference memories with full content', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'code style',
|
||||
results: {
|
||||
contexts: [],
|
||||
experiences: [],
|
||||
preferences: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
conclusionDirectives: 'Always use TypeScript strict mode',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
id: 'pref-1',
|
||||
metadata: null,
|
||||
scorePriority: 10,
|
||||
suggestions: 'Consider adding eslint rules',
|
||||
tags: ['typescript', 'code-quality'],
|
||||
type: 'coding-standard',
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should format mixed results with all memory types', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'work',
|
||||
results: {
|
||||
contexts: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
associatedObjects: null,
|
||||
associatedSubjects: null,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
currentStatus: null,
|
||||
description: 'Context description',
|
||||
id: 'ctx-1',
|
||||
metadata: null,
|
||||
scoreImpact: null,
|
||||
scoreUrgency: null,
|
||||
tags: null,
|
||||
title: 'Context Title',
|
||||
type: null,
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryIds: null,
|
||||
},
|
||||
],
|
||||
experiences: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
action: null,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
id: 'exp-1',
|
||||
keyLearning: 'Key learning',
|
||||
metadata: null,
|
||||
possibleOutcome: null,
|
||||
reasoning: null,
|
||||
scoreConfidence: null,
|
||||
situation: 'Situation',
|
||||
tags: null,
|
||||
type: null,
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryId: null,
|
||||
},
|
||||
],
|
||||
preferences: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
conclusionDirectives: 'Directive',
|
||||
createdAt: new Date('2024-01-01'),
|
||||
id: 'pref-1',
|
||||
metadata: null,
|
||||
scorePriority: null,
|
||||
suggestions: null,
|
||||
tags: null,
|
||||
type: null,
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle null and undefined values gracefully', () => {
|
||||
const result = formatMemorySearchResults({
|
||||
query: 'test',
|
||||
results: {
|
||||
contexts: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01'),
|
||||
associatedObjects: null,
|
||||
associatedSubjects: null,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
currentStatus: null,
|
||||
description: null,
|
||||
id: 'ctx-1',
|
||||
metadata: null,
|
||||
scoreImpact: null,
|
||||
scoreUrgency: null,
|
||||
tags: null,
|
||||
title: null,
|
||||
type: null,
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
userMemoryIds: null,
|
||||
},
|
||||
],
|
||||
experiences: [],
|
||||
preferences: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
164
packages/prompts/src/prompts/userMemory/formatSearchResults.ts
Normal file
164
packages/prompts/src/prompts/userMemory/formatSearchResults.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { SearchMemoryResult } from '@lobechat/types';
|
||||
|
||||
/**
|
||||
* Search result item interfaces matching the SearchMemoryResult type
|
||||
*/
|
||||
type ContextResult = SearchMemoryResult['contexts'][number];
|
||||
type ExperienceResult = SearchMemoryResult['experiences'][number];
|
||||
type PreferenceResult = SearchMemoryResult['preferences'][number];
|
||||
|
||||
/**
|
||||
* Format a single context memory item for search results
|
||||
* Format: attributes for metadata, description as text content
|
||||
*/
|
||||
const formatContextResult = (item: ContextResult): string => {
|
||||
const attrs: string[] = [`id="${item.id}"`];
|
||||
|
||||
if (item.title) {
|
||||
attrs.push(`title="${item.title}"`);
|
||||
}
|
||||
if (item.scoreUrgency !== null && item.scoreUrgency !== undefined) {
|
||||
attrs.push(`urgency=${item.scoreUrgency}`);
|
||||
}
|
||||
if (item.scoreImpact !== null && item.scoreImpact !== undefined) {
|
||||
attrs.push(`impact=${item.scoreImpact}`);
|
||||
}
|
||||
if (item.type) {
|
||||
attrs.push(`type="${item.type}"`);
|
||||
}
|
||||
if (item.currentStatus) {
|
||||
attrs.push(`status="${item.currentStatus}"`);
|
||||
}
|
||||
|
||||
const children: string[] = [];
|
||||
|
||||
// Description as main text content
|
||||
if (item.description) {
|
||||
children.push(` ${item.description}`);
|
||||
}
|
||||
|
||||
// Associated subjects (actors)
|
||||
if (item.associatedSubjects && item.associatedSubjects.length > 0) {
|
||||
const subjects = item.associatedSubjects
|
||||
.filter((s) => s?.name)
|
||||
.map((s) => `${s.name}${s.type ? ` (${s.type})` : ''}`)
|
||||
.join(', ');
|
||||
if (subjects) {
|
||||
children.push(` <subjects>${subjects}</subjects>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Associated objects (resources)
|
||||
if (item.associatedObjects && item.associatedObjects.length > 0) {
|
||||
const objects = item.associatedObjects
|
||||
.filter((o) => o?.name)
|
||||
.map((o) => `${o.name}${o.type ? ` (${o.type})` : ''}`)
|
||||
.join(', ');
|
||||
if (objects) {
|
||||
children.push(` <objects>${objects}</objects>`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = children.length > 0 ? `\n${children.join('\n')}\n ` : '';
|
||||
|
||||
return ` <context ${attrs.join(' ')}>${content}</context>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a single experience memory item for search results
|
||||
* Format: attributes for metadata, situation and keyLearning as child elements
|
||||
*/
|
||||
const formatExperienceResult = (item: ExperienceResult): string => {
|
||||
const attrs: string[] = [`id="${item.id}"`];
|
||||
|
||||
if (item.type) {
|
||||
attrs.push(`type="${item.type}"`);
|
||||
}
|
||||
if (item.scoreConfidence !== null && item.scoreConfidence !== undefined) {
|
||||
attrs.push(`confidence=${item.scoreConfidence}`);
|
||||
}
|
||||
|
||||
const children: string[] = [];
|
||||
|
||||
if (item.situation) {
|
||||
children.push(` <situation>${item.situation}</situation>`);
|
||||
}
|
||||
if (item.keyLearning) {
|
||||
children.push(` <keyLearning>${item.keyLearning}</keyLearning>`);
|
||||
}
|
||||
|
||||
const content = children.length > 0 ? `\n${children.join('\n')}\n ` : '';
|
||||
|
||||
return ` <experience ${attrs.join(' ')}>${content}</experience>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a single preference memory item for search results
|
||||
* Format: attributes for metadata, directives as text content
|
||||
*/
|
||||
const formatPreferenceResult = (item: PreferenceResult): string => {
|
||||
const attrs: string[] = [`id="${item.id}"`];
|
||||
|
||||
if (item.type) {
|
||||
attrs.push(`type="${item.type}"`);
|
||||
}
|
||||
if (item.scorePriority !== null && item.scorePriority !== undefined) {
|
||||
attrs.push(`priority=${item.scorePriority}`);
|
||||
}
|
||||
|
||||
const content = item.conclusionDirectives || '';
|
||||
|
||||
return ` <preference ${attrs.join(' ')}>${content}</preference>`;
|
||||
};
|
||||
|
||||
export interface FormatSearchResultsOptions {
|
||||
/** The search query that was used */
|
||||
query: string;
|
||||
/** The search results to format */
|
||||
results: SearchMemoryResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format memory search results as XML for LLM consumption.
|
||||
*
|
||||
* This function formats the complete search results with all content details,
|
||||
* making the retrieved memories directly usable by the LLM for reasoning
|
||||
* and response generation.
|
||||
*/
|
||||
export const formatMemorySearchResults = ({
|
||||
query,
|
||||
results,
|
||||
}: FormatSearchResultsOptions): string => {
|
||||
const { contexts, experiences, preferences } = results;
|
||||
const total = contexts.length + experiences.length + preferences.length;
|
||||
|
||||
if (total === 0) {
|
||||
return `<memories query="${query}">
|
||||
<status>No memories found matching the query.</status>
|
||||
</memories>`;
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
// Add contexts section
|
||||
if (contexts.length > 0) {
|
||||
const contextsXml = contexts.map(formatContextResult).join('\n');
|
||||
sections.push(`<contexts count="${contexts.length}">\n${contextsXml}\n</contexts>`);
|
||||
}
|
||||
|
||||
// Add experiences section
|
||||
if (experiences.length > 0) {
|
||||
const experiencesXml = experiences.map(formatExperienceResult).join('\n');
|
||||
sections.push(`<experiences count="${experiences.length}">\n${experiencesXml}\n</experiences>`);
|
||||
}
|
||||
|
||||
// Add preferences section
|
||||
if (preferences.length > 0) {
|
||||
const preferencesXml = preferences.map(formatPreferenceResult).join('\n');
|
||||
sections.push(`<preferences count="${preferences.length}">\n${preferencesXml}\n</preferences>`);
|
||||
}
|
||||
|
||||
return `<memories query="${query}" total="${total}">
|
||||
${sections.join('\n')}
|
||||
</memories>`;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './formatSearchResults';
|
||||
|
||||
/**
|
||||
* User memory item interfaces
|
||||
*/
|
||||
|
||||
@@ -38,15 +38,6 @@ const ToolTitle = memo<ToolTitleProps>(({ identifier, apiName, isLoading, isAbor
|
||||
const isBuiltinPlugin = builtinToolIdentifiers.includes(identifier);
|
||||
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
||||
|
||||
// Debug logging for LobeHub Skill title issue
|
||||
console.log('[ToolTitle Debug]', {
|
||||
apiName,
|
||||
identifier,
|
||||
isBuiltinPlugin,
|
||||
pluginMeta,
|
||||
pluginTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
|
||||
@@ -20,7 +20,6 @@ const Copilot = memo(() => {
|
||||
s.updateSystemStatus,
|
||||
]);
|
||||
|
||||
console.log('defaultWidth:', width);
|
||||
return (
|
||||
<RightPanel
|
||||
defaultWidth={width}
|
||||
|
||||
Reference in New Issue
Block a user