Skip to content

Commit

Permalink
✨ feat: support markdown type plugin (#865)
Browse files Browse the repository at this point in the history
* ✨ feat: support markdown type plugin

* 🚨 ci: fix ci

* 🚨 ci: fix test

* ✨ feat: support trigger AI message and create assistant message

* ✅ test: add unit tests

* 📸 test: update test

* 📸 test: update test

* 💄 style: improve loading style
  • Loading branch information
arvinxx committed Dec 29, 2023
1 parent baaf06a commit 2791166
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 47 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"@types/ua-parser-js": "^0.7",
"@types/uuid": "^9",
"@umijs/lint": "^4",
"@vitest/coverage-v8": "0.34.6",
"@vitest/coverage-v8": "^1",
"commitlint": "^18",
"consola": "^3",
"dpdm": "^3",
Expand All @@ -175,7 +175,7 @@
"typescript": "^5",
"unified": "^11",
"unist-util-visit": "^5",
"vitest": "0.34.6",
"vitest": "^1",
"vitest-canvas-mock": "^0.3.3"
},
"publishConfig": {
Expand Down
2 changes: 1 addition & 1 deletion src/database/schemas/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const PluginSchema = z.object({
identifier: z.string(),
arguments: z.string(),
apiName: z.string(),
type: z.enum(['default', 'standalone', 'builtin']).default('default'),
type: z.enum(['default', 'markdown', 'standalone', 'builtin']).default('default'),
});

export const DB_MessageSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { BuiltinToolsRenders } from '@/tools/renders';

Expand All @@ -17,13 +16,7 @@ const BuiltinType = memo<BuiltinTypeProps>(({ content, id, identifier, loading }
const { isJSON, data } = useParseContent(content);

if (!isJSON) {
return (
loading && (
<Flexbox gap={8}>
<Loading />
</Flexbox>
)
);
return loading && <Loading />;
}

const Render = BuiltinToolsRenders[identifier || ''];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Skeleton } from 'antd';
import dynamic from 'next/dynamic';
import { Suspense, memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useToolStore } from '@/store/tool';
import { pluginSelectors } from '@/store/tool/selectors';
Expand All @@ -24,13 +23,7 @@ const PluginDefaultType = memo<PluginDefaultTypeProps>(({ content, name, loading
const { isJSON, data } = useParseContent(content);

if (!isJSON) {
return (
loading && (
<Flexbox gap={8}>
<Loading />
</Flexbox>
)
);
return loading && <Loading />;
}

if (!manifest?.ui) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Markdown } from '@lobehub/ui';
import { memo } from 'react';

import Loading from '../Loading';

export interface PluginMarkdownTypeProps {
content: string;
loading?: boolean;
}

const PluginMarkdownType = memo<PluginMarkdownTypeProps>(({ content, loading }) => {
if (loading) return <Loading />;

return <Markdown>{content}</Markdown>;
});

export default PluginMarkdownType;
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { pluginSelectors } from '@/store/tool/selectors';

import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
import {
useOnPluginCreateAssistantMessage,
useOnPluginFetchMessage,
useOnPluginFetchPluginSettings,
useOnPluginFetchPluginState,
useOnPluginFillContent,
useOnPluginTriggerAIMessage,
} from '../utils/listenToPlugin';
import { useOnPluginSettingsUpdate } from '../utils/pluginSettings';
import { useOnPluginStateUpdate } from '../utils/pluginState';
Expand Down Expand Up @@ -118,6 +120,21 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
updatePluginSettings(payload?.identifier, value);
});

// when plugin want to trigger AI message
const triggerAIMessage = useChatStore((s) => s.triggerAIMessage);
useOnPluginTriggerAIMessage((messageId) => {
// we need to know which message to trigger
if (messageId !== id) return;

triggerAIMessage(id);
});

// when plugin want to create an assistant message
const createAssistantMessage = useChatStore((s) => s.createAssistantMessageByPlugin);
useOnPluginCreateAssistantMessage((content) => {
createAssistantMessage(content, id);
});

return (
<>
{loading && <Skeleton active style={{ maxWidth: '100%', width }} />}
Expand Down
5 changes: 5 additions & 0 deletions src/features/Conversation/ChatList/Plugins/Render/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LobeToolRenderType } from '@/types/tool';

import BuiltinType from '././BuiltinType';
import DefaultType from './DefaultType';
import Markdown from './MarkdownType';
import Standalone from './StandaloneType';

export interface PluginRenderProps {
Expand All @@ -27,6 +28,10 @@ const PluginRender = memo<PluginRenderProps>(
return <BuiltinType content={content} id={id} identifier={identifier} loading={loading} />;
}

case 'markdown': {
return <Markdown content={content} loading={loading} />;
}

default: {
return <DefaultType content={content} loading={loading} name={identifier} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import {
useOnPluginCreateAssistantMessage,
useOnPluginFetchMessage,
useOnPluginFetchPluginSettings,
useOnPluginFetchPluginState,
useOnPluginFillContent,
useOnPluginTriggerAIMessage,
} from './listenToPlugin';

afterEach(() => {
Expand Down Expand Up @@ -102,3 +104,61 @@ describe('useOnPluginFetchPluginSettings', () => {
expect(mockOnRequest).toHaveBeenCalled();
});
});

describe('useOnPluginTriggerAIMessage', () => {
it('calls callback with id when a triggerAIMessage is received', () => {
const mockCallback = vi.fn();
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));

const testId = 'testId';
const event = new MessageEvent('message', {
data: { type: PluginChannel.triggerAIMessage, id: testId },
});

window.dispatchEvent(event);

expect(mockCallback).toHaveBeenCalledWith(testId);
});

it('does not call callback for other message types', () => {
const mockCallback = vi.fn();
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));

const event = new MessageEvent('message', {
data: { type: 'otherMessageType', id: 'testId' },
});

window.dispatchEvent(event);

expect(mockCallback).not.toHaveBeenCalled();
});
});

describe('useOnPluginCreateAssistantMessage', () => {
it('calls callback with content when a createAssistantMessage is received', () => {
const mockCallback = vi.fn();
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));

const testContent = 'testContent';
const event = new MessageEvent('message', {
data: { type: PluginChannel.createAssistantMessage, content: testContent },
});

window.dispatchEvent(event);

expect(mockCallback).toHaveBeenCalledWith(testContent);
});

it('does not call callback for other message types', () => {
const mockCallback = vi.fn();
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));

const event = new MessageEvent('message', {
data: { type: 'otherMessageType', content: 'testContent' },
});

window.dispatchEvent(event);

expect(mockCallback).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,33 @@ export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
};
}, []);
};

export const useOnPluginTriggerAIMessage = (callback: (id: string) => void) => {
useEffect(() => {
const fn = (e: MessageEvent) => {
if (e.data.type === PluginChannel.triggerAIMessage) {
callback(e.data.id);
}
};

window.addEventListener('message', fn);
return () => {
window.removeEventListener('message', fn);
};
}, []);
};

export const useOnPluginCreateAssistantMessage = (callback: (content: string) => void) => {
useEffect(() => {
const fn = (e: MessageEvent) => {
if (e.data.type === PluginChannel.createAssistantMessage) {
callback(e.data.content);
}
};

window.addEventListener('message', fn);
return () => {
window.removeEventListener('message', fn);
};
}, []);
};
26 changes: 13 additions & 13 deletions src/services/__tests__/__snapshots__/plugin.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ General guidelines:
- Inform users if information is not from Wolfram endpoints.
- Display image URLs with Markdown syntax: ![URL]
- ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
- ALWAYS use {\\"input\\": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\\\n[expression]\\\\n$$' for standalone cases and '\\\\( [expression] \\\\)' when inline.
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\n[expression]\\n$$' for standalone cases and '\\( [expression] \\)' when inline.
- Format inline Wolfram Language code with Markdown code formatting.
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
getWolframAlphaResults guidelines:
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
- Convert inputs to simplified keyword queries whenever possible (e.g. convert \\"how many people live in France\\" to \\"France population\\").
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
- Include a space between compound units (e.g., \\"Ω m\\" for \\"ohm*meter\\").
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
- If data for multiple properties is needed, make separate calls for each property.
- If a Wolfram Alpha result is not relevant to the query:
Expand All @@ -55,22 +55,22 @@ getWolframCloudResults guidelines:
- Accepts only syntactically correct Wolfram Language code.
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.
- Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
-- Find the EntityType that represents countries: \`Interpreter[\\"EntityType\\",AmbiguityFunction->All][\\"countries\\"]\`.
-- Find the Entity for the Empire State Building: \`Interpreter[\\"Building\\",AmbiguityFunction->All][\\"empire state\\"]\`.
-- EntityClasses: Find the \\"Movie\\" entity class for Star Trek movies: \`Interpreter[\\"MovieClass\\",AmbiguityFunction->All][\\"star trek\\"]\`.
-- Find EntityProperties associated with \\"weight\\" of \\"Element\\" entities: \`Interpreter[Restricted[\\"EntityProperty\\", \\"Element\\"],AmbiguityFunction->All][\\"weight\\"]\`.
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation[\\"skyscrapers\\",_,Hold,AmbiguityFunction->All]\`.
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity[\\"Element\\",\\"Gold\\"][\\"AtomicNumber\\"]\` to \`ElementData[\\"Gold\\",\\"AtomicNumber\\"]\`).
-- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
-- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
-- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
-- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
- When composing code:
-- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
-- Use Association to organize and manipulate data when appropriate.
-- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
-- Use only camel case for variable names (e.g., variableName).
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {\\"sin(x)\\", \\"cos(x)\\", \\"tan(x)\\"}\`).
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
-- Avoid use of QuantityMagnitude.
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity[\\"WolframLanguageSymbol\\",symbol],{\\"PlaintextUsage\\",\\"Options\\"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
-- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
- Remove all comments and formatting from code passed to the \\"input\\" parameter; for example: instead of \`square[x_] := Module[{result},\\\\n result = x^2 (* Calculate the square *)\\\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
- Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\\n result = x^2 (* Calculate the square *)\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
- In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
",
"type": "default",
Expand Down
5 changes: 2 additions & 3 deletions src/store/chat/slices/message/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { StateCreator } from 'zustand/vanilla';
import { GPT4_VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/llm';
import { LOADING_FLAT, isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/const/message';
import { CreateMessageParams } from '@/database/models/message';
import { DB_Message } from '@/database/schemas/message';
import { chatService } from '@/services/chat';
import { messageService } from '@/services/message';
import { topicService } from '@/services/topic';
Expand Down Expand Up @@ -238,7 +237,7 @@ export const chatMessage: StateCreator<
const { model } = getAgentConfig();

// 1. Add an empty message to place the AI response
const assistantMessage: DB_Message = {
const assistantMessage: CreateMessageParams = {
role: 'assistant',
content: LOADING_FLAT,
fromModel: model,
Expand Down Expand Up @@ -383,7 +382,7 @@ export const chatMessage: StateCreator<
toggleChatLoading(false, undefined, n('generateMessage(end)') as string);

// also exist message like this:
// 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
// 请稍等,我帮您查询一下。{"tool_calls": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
if (!isFunctionCall) {
const { content, valid } = testFunctionMessageAtEnd(output);

Expand Down
Loading

0 comments on commit 2791166

Please sign in to comment.