Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: support markdown type plugin #865

Merged
merged 8 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading