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

🚧 wip: Dify integration & Agnet code refactor #4404

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions src/app/(backend)/webapi/chat/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const runtime = 'edge';

export const POST = checkAuth(async (req: Request, { params, jwtPayload, createRuntime }) => {
const { provider } = params;
console.log(jwtPayload);

try {
// ============ 1. init chat model ============ //
Expand Down
8 changes: 7 additions & 1 deletion src/const/settings/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {

export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
chatConfig: DEFAULT_AGENT_CHAT_CONFIG,
dify: {
baseUrl: 'https://api.dify.ai/v1',
enabled: false,
token: '',
userId: 'devTest',
},
model: DEFAULT_MODEL,
params: {
frequency_penalty: 0,
Expand All @@ -32,7 +38,7 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
plugins: [],
provider: ModelProvider.OpenAI,
systemRole: '',
tts: DEFAUTT_AGENT_TTS_CONFIG,
tts: DEFAUTT_AGENT_TTS_CONFIG
};

export const DEFAULT_AGENT: UserDefaultAgent = {
Expand Down
2 changes: 2 additions & 0 deletions src/features/AgentSetting/AgentSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AgentModal from './AgentModal';
import AgentPlugin from './AgentPlugin';
import AgentPrompt from './AgentPrompt';
import AgentTTS from './AgentTTS';
import Dify from './Dify'
import StoreUpdater, { StoreUpdaterProps } from './StoreUpdater';
import { Provider, createStore } from './store';

Expand All @@ -19,6 +20,7 @@ export const AgentSettings = (props: AgentSettingsProps) => {
<AgentModal />
<AgentTTS />
<AgentPlugin />
<Dify />
</Provider>
);
};
91 changes: 91 additions & 0 deletions src/features/AgentSetting/Dify/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

import { Form, type ItemGroup, Input } from '@lobehub/ui';
import { Switch } from 'antd';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { FORM_STYLE } from '@/const/layoutTokens';

import { useStore } from '../store';
import { useUserStore } from '@/store/user';

const AgentMeta = memo(() => {
const { t } = useTranslation('setting');

const [setAgentConfig, agentConfig ] = useStore((s) => [
s.setAgentConfig,
s.config
]);

const updateKeyVaultConfig = useUserStore((s)=>s.updateKeyVaultConfig)

const [difyBaseUrl, setDifyBaseUrl] = useState<string>('https://api.dify.ai/v1')
const [difyToken, setDifyToken] = useState<string>('app-1WHrUsnegCQqYgSjN952dHyt')
const [difyUserId, setDifyUserId] = useState<string>('dev')
const [difyEnabled, setDifyEnable] = useState<boolean>(true)

useEffect(() => {
setAgentConfig({
dify: {
baseUrl: difyBaseUrl,
token: difyToken,
userId: difyUserId,
enabled: difyEnabled,
},
provider: 'dify',
model: 'Dofy Workflow'
})
updateKeyVaultConfig('dify', {
baseUrl: difyBaseUrl,
token: difyToken,
userId: difyUserId,
})
}, [difyBaseUrl, difyToken, difyUserId, difyEnabled])

const metaData: ItemGroup = {
children: [
{
children: (
<Input
onChange={(event) => setDifyBaseUrl(event.currentTarget.value)}
placeholder='https://api.dify.ai/v1'
value={agentConfig.dify.baseUrl} />
),
label: 'BaseUrl'
},
{
children: (
<Input
onChange={(event) => setDifyToken(event.currentTarget.value)}
value={agentConfig.dify.token}
/>
),
label: 'Token'
},
{
children: (
<Input
onChange={(event) => setDifyUserId(event.currentTarget.value)}
value={agentConfig.dify.userId}
/>
),
label: 'UserId'
},
{
children: (
<Switch
onChange={setDifyEnable}
value={agentConfig.dify.enabled}
/>
),
label: 'BaseUrl'
}
],
title: t('settingAgent.title'),
};

return <Form items={[metaData]} itemsType={'group'} variant={'pure'} {...FORM_STYLE} />;
});

export default AgentMeta;
3 changes: 2 additions & 1 deletion src/features/AgentSetting/store/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export interface Action extends PublicAction {
resetAgentMeta: () => void;
setAgentConfig: (config: DeepPartial<LobeAgentConfig>) => void;
setAgentMeta: (meta: Partial<MetaData>) => void;

setChatConfig: (config: Partial<LobeAgentChatConfig>) => void;

streamUpdateMetaArray: (key: keyof MetaData) => any;
streamUpdateMetaString: (key: keyof MetaData) => any;
toggleAgentPlugin: (pluginId: string, state?: boolean) => void;
Expand Down Expand Up @@ -291,6 +291,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
};
},


toggleAgentPlugin: (id, state) => {
get().dispatchConfig({ pluginId: id, state, type: 'togglePlugin' });
},
Expand Down
7 changes: 7 additions & 0 deletions src/libs/agent-runtime/AgentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { LobeUpstageAI } from './upstage';
import { LobeZeroOneAI } from './zeroone';
import { LobeZhipuAI } from './zhipu';
import LobeDify from './dify';

export interface AgentChatOptions {
enableTrace?: boolean;
Expand Down Expand Up @@ -154,6 +155,7 @@ class AgentRuntime {
upstage: Partial<ClientOptions>;
zeroone: Partial<ClientOptions>;
zhipu: Partial<ClientOptions>;
dify: Partial<{ baseUrl: string; token: string; userId: string; conversation_id: string}>;
}>,
) {
let runtimeModel: LobeRuntimeAI;
Expand Down Expand Up @@ -314,6 +316,11 @@ class AgentRuntime {
runtimeModel = new LobeHunyuanAI(params.hunyuan);
break;
}

case ModelProvider.Dify: {
runtimeModel = new LobeDify(params.dify || {})
break
}
}

return new AgentRuntime(runtimeModel);
Expand Down
75 changes: 75 additions & 0 deletions src/libs/agent-runtime/dify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { LobeRuntimeAI } from '../BaseAI';
import { AgentRuntimeErrorType } from '../error';
import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
import { AgentRuntimeError } from '../utils/createError';
import { StreamingResponse } from '../utils/response';
import { DifyStream } from '../utils/streams/dify';
import urlJoin from 'url-join';

export interface DifyParams {
baseUrl: string
token?: string
userId: string
conversation_id?: string
}

export class LobeDify implements LobeRuntimeAI {
difyParams: DifyParams

constructor({ baseUrl, token, userId, conversation_id }: Partial<DifyParams>) {
if (!(userId && token))
throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);

this.difyParams = {
baseUrl: baseUrl ?? 'https://api.dify.ai/v1',
conversation_id,
userId,
token,
}
}

async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
const { messages } = payload
// Get the last message as query
const query = messages.at(-1)
if (!query) {
throw new Error('[Dify]: No query')
}
let textQuery = ''
if (typeof query.content === 'string')
textQuery = query.content
else
throw new Error('[Dify]: Unsupport user message type')

const response = await fetch(urlJoin(this.difyParams.baseUrl, '/chat-messages'), {
method: 'POST',
// signal: options?.signal,
headers: {
Authorization: `Bearer ${this.difyParams.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: textQuery,
user: this.difyParams.userId,
conversation_id: this.difyParams?.conversation_id ?? '',
response_mode: 'streaming',
inputs: [],
files: [],
}),
})
if (!response.body || !response.ok) {
throw AgentRuntimeError.chat({
error: {
status: response.status,
statusText: response.statusText,
},
errorType: AgentRuntimeErrorType.ProviderBizError,
provider: ModelProvider.Dify,
});
}
const [prod, _] = response.body.tee();
return StreamingResponse(DifyStream(prod), { headers: options?.headers });
}
}

export default LobeDify;
1 change: 1 addition & 0 deletions src/libs/agent-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './types';
export { AgentRuntimeError } from './utils/createError';
export { LobeZeroOneAI } from './zeroone';
export { LobeZhipuAI } from './zhipu';
export { LobeDify } from './dify'
1 change: 1 addition & 0 deletions src/libs/agent-runtime/types/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export enum ModelProvider {
Wenxin = 'wenxin',
ZeroOne = 'zeroone',
ZhiPu = 'zhipu',
Dify = 'dify'
}

export type ModelProviderKey = Lowercase<keyof typeof ModelProvider>;
74 changes: 74 additions & 0 deletions src/libs/agent-runtime/utils/streams/dify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createCallbacksTransformer, createSSEProtocolTransformer, StreamProtocolChunk } from ".";
import { ChatStreamCallbacks } from "../..";

interface DifyChunk {
event: string;
task_id?: string;
answer?: string;
message?: string;
message_id?: string;
id?: string
}

const processDifyData = (buffer: string): DifyChunk | undefined => {
try {
// Remove the prefix `data:`
if (buffer.startsWith('data:'))
return JSON.parse(buffer.slice(5).trim()) as DifyChunk
return JSON.parse(buffer.trim())
} catch (error) {
// Try another way to parse the data
// Dify is wired, sometimes the stream data contains multiple lines of
// stream event in a single chunk, so we need to split the data by `data:`
// and ONLY keep the last slice of the chunk
const slices = buffer.split('data: ');
try {
return JSON.parse(slices[slices.length - 1].trim()) as DifyChunk
} catch { }
}
}

export const transformDifyStream = (buffer: Uint8Array): StreamProtocolChunk => {
const decoder = new TextDecoder()
const chunk = processDifyData(decoder.decode(buffer, { stream: true }))
const id = chunk?.message_id ?? chunk?.task_id ?? chunk?.id;
// Return empty block if error
if (!chunk || !id) return {
data: '',
type: 'text',
}
let type: StreamProtocolChunk['type'] = 'text';
let data: DifyChunk | string = chunk;
switch (chunk.event) {
case 'message_end':
type = 'stop'
break;
case 'message':
type = 'text';
data = chunk.answer ?? '';
break;
case 'workflow_started':
type = 'tool_using';
break;
case 'node_started':
type = 'thoughts';
break;
case 'workflow_finished':
type = 'tool_using';
break;
case 'node_finished':
type = 'thoughts';
break;
}
return {
id,
data,
type,
}
}

export const DifyStream = (stream: ReadableStream, callbacks?: ChatStreamCallbacks) => {
return stream
.pipeThrough(createSSEProtocolTransformer(transformDifyStream))
.pipeThrough(createCallbacksTransformer(callbacks));
};
6 changes: 5 additions & 1 deletion src/libs/agent-runtime/utils/streams/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export interface StreamStack {
export interface StreamProtocolChunk {
data: any;
id?: string;
type: 'text' | 'tool_calls' | 'data' | 'stop' | 'error';
/**
* - thoughts: the thoughts of the AI
* - tool_using: the tool is using
*/
type: 'text' | 'tool_calls' | 'data' | 'stop' | 'error' | 'thoughts' | 'tool_using'
}

export interface StreamToolCallChunkData {
Expand Down
3 changes: 3 additions & 0 deletions src/server/modules/AgentRuntime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {

return { apiKey };
}
case ModelProvider.Dify: {
return payload
}
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/services/_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export const getProviderAuthPayload = (provider: string) => {
return { endpoint: config?.baseURL };
}

case ModelProvider.Dify: {
const { token, baseUrl, userId } = keyVaultsConfigSelectors.difyConfig(useUserStore.getState())
return {
token,
baseUrl,
userId,
}
}

default: {
const config = keyVaultsConfigSelectors.getVaultByProvider(provider as GlobalLLMProviderKey)(
useUserStore.getState(),
Expand Down
Loading
Loading