Unverified Commit 631a4f1f authored by Joel's avatar Joel Committed by GitHub

Merge pull request #42 from langgenius/feat/support-agent

feat: support agent
parents 3de8abdd 25ba4ac4
import { type NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest, { params }: {
params: { conversationId: string }
}) {
const body = await request.json()
const {
auto_generate,
name,
} = body
const { conversationId } = params
const { user } = getInfo(request)
// auto generate name
const { data } = await client.renameConversation(conversationId, name, user, auto_generate)
return NextResponse.json(data)
}
...@@ -3,7 +3,7 @@ import classNames from 'classnames' ...@@ -3,7 +3,7 @@ import classNames from 'classnames'
import style from './style.module.css' import style from './style.module.css'
export type AppIconProps = { export type AppIconProps = {
size?: 'tiny' | 'small' | 'medium' | 'large' size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large'
rounded?: boolean rounded?: boolean
icon?: string icon?: string
background?: string background?: string
......
.appIcon { .appIcon {
@apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0; @apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0;
} }
.appIcon.large { .appIcon.large {
@apply w-10 h-10; @apply w-10 h-10;
} }
.appIcon.small { .appIcon.small {
@apply w-8 h-8; @apply w-8 h-8;
} }
.appIcon.xs {
@apply w-3 h-3 text-base;
}
.appIcon.tiny { .appIcon.tiny {
@apply w-6 h-6 text-base; @apply w-6 h-6 text-base;
} }
.appIcon.rounded { .appIcon.rounded {
@apply rounded-full; @apply rounded-full;
} }
\ No newline at end of file
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "chevron-down"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M3 4.5L6 7.5L9 4.5",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "ChevronDown"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './data.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ChevronDown'
export default Icon
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#clip0_7847_32895)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.5 2.5C10.5 3.32843 8.48528 4 6 4C3.51472 4 1.5 3.32843 1.5 2.5M10.5 2.5C10.5 1.67157 8.48528 1 6 1C3.51472 1 1.5 1.67157 1.5 2.5M10.5 2.5V9.5C10.5 10.33 8.5 11 6 11C3.5 11 1.5 10.33 1.5 9.5V2.5M10.5 6C10.5 6.83 8.5 7.5 6 7.5C3.5 7.5 1.5 6.83 1.5 6",
"stroke": "#667085",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_7847_32895"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "12",
"height": "12",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "DataSet"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './data.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'DataSet'
export default Icon
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "check-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "CheckCircle"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './data.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'CheckCircle'
export default Icon
...@@ -6,10 +6,13 @@ import { useTranslation } from 'react-i18next' ...@@ -6,10 +6,13 @@ import { useTranslation } from 'react-i18next'
import LoadingAnim from '../loading-anim' import LoadingAnim from '../loading-anim'
import type { FeedbackFunc, IChatItem } from '../type' import type { FeedbackFunc, IChatItem } from '../type'
import s from '../style.module.css' import s from '../style.module.css'
import ImageGallery from '../../base/image-gallery'
import Thought from '../thought'
import { randomString } from '@/utils/string' import { randomString } from '@/utils/string'
import type { MessageRating } from '@/types/app' import type { MessageRating, VisionFile } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import type { Emoji } from '@/types/tools'
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => ( const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
<div <div
...@@ -55,11 +58,20 @@ type IAnswerProps = { ...@@ -55,11 +58,20 @@ type IAnswerProps = {
feedbackDisabled: boolean feedbackDisabled: boolean
onFeedback?: FeedbackFunc onFeedback?: FeedbackFunc
isResponsing?: boolean isResponsing?: boolean
allToolIcons?: Record<string, string | Emoji>
} }
// The component needs to maintain its own state to control whether to display input component // The component needs to maintain its own state to control whether to display input component
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback, isResponsing }) => { const Answer: FC<IAnswerProps> = ({
const { id, content, feedback } = item item,
feedbackDisabled = false,
onFeedback,
isResponsing,
allToolIcons,
}) => {
const { id, content, feedback, agent_thoughts } = item
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0
const { t } = useTranslation() const { t } = useTranslation()
/** /**
...@@ -121,6 +133,37 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback, ...@@ -121,6 +133,37 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
) )
} }
const getImgs = (list?: VisionFile[]) => {
if (!list)
return []
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
}
const agentModeAnswer = (
<div>
{agent_thoughts?.map((item, index) => (
<div key={index}>
{item.thought && (
<Markdown content={item.thought} />
)}
{/* {item.tool} */}
{/* perhaps not use tool */}
{!!item.tool && (
<Thought
thought={item}
allToolIcons={allToolIcons || {}}
isFinished={!!item.observation || !isResponsing}
/>
)}
{getImgs(item.message_files).length > 0 && (
<ImageGallery srcs={getImgs(item.message_files).map(item => item.url)} />
)}
</div>
))}
</div>
)
return ( return (
<div key={id}> <div key={id}>
<div className='flex items-start'> <div className='flex items-start'>
...@@ -134,21 +177,17 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback, ...@@ -134,21 +177,17 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
<div className={`${s.answerWrap}`}> <div className={`${s.answerWrap}`}>
<div className={`${s.answer} relative text-sm text-gray-900`}> <div className={`${s.answer} relative text-sm text-gray-900`}>
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}> <div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
{item.isOpeningStatement && ( {(isResponsing && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
<div className='flex items-center mb-1 gap-1'>
<OpeningStatementIcon />
<div className='text-xs text-gray-500'>{t('app.chat.openingStatementTitle')}</div>
</div>
)}
{(isResponsing && !content)
? ( ? (
<div className='flex items-center justify-center w-6 h-5'> <div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' /> <LoadingAnim type='text' />
</div> </div>
) )
: ( : (isAgentMode
<Markdown content={content} /> ? agentModeAnswer
)} : (
<Markdown content={content} />
))}
</div> </div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'> <div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()} {!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}
......
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ThoughtItem, ToolInfoInThought } from '../type'
import Tool from './tool'
import type { Emoji } from '@/types/tools'
export type IThoughtProps = {
thought: ThoughtItem
allToolIcons: Record<string, string | Emoji>
isFinished: boolean
}
function getValue(value: string, isValueArray: boolean, index: number) {
if (isValueArray) {
try {
return JSON.parse(value)[index]
}
catch (e) {
}
}
return value
}
const Thought: FC<IThoughtProps> = ({
thought,
allToolIcons,
isFinished,
}) => {
const [toolNames, isValueArray]: [string[], boolean] = (() => {
try {
if (Array.isArray(JSON.parse(thought.tool)))
return [JSON.parse(thought.tool), true]
}
catch (e) {
}
return [[thought.tool], false]
})()
const toolThoughtList = toolNames.map((toolName, index) => {
return {
name: toolName,
input: getValue(thought.tool_input, isValueArray, index),
output: getValue(thought.observation, isValueArray, index),
isFinished,
}
})
return (
<div className='my-2 space-y-2'>
{toolThoughtList.map((item: ToolInfoInThought, index) => (
<Tool
key={index}
payload={item}
allToolIcons={allToolIcons}
/>
))}
</div>
)
}
export default React.memo(Thought)
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
isRequest: boolean
toolName: string
content: string
}
const Panel: FC<Props> = ({
isRequest,
toolName,
content,
}) => {
const { t } = useTranslation()
return (
<div className='rounded-md bg-gray-100 overflow-hidden border border-black/5'>
<div className='flex items-center px-2 py-1 leading-[18px] bg-gray-50 uppercase text-xs font-medium text-gray-500'>
{t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName}
</div>
<div className='p-2 border-t border-black/5 leading-4 text-xs text-gray-700'>{content}</div>
</div>
)
}
export default React.memo(Panel)
.wrap {
background-color: rgba(255, 255, 255, 0.92);
}
.wrapHoverEffect:hover{
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
}
\ No newline at end of file
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { ToolInfoInThought } from '../type'
import Panel from './panel'
import Loading02 from '@/app/components/base/icons/line/loading-02'
import ChevronDown from '@/app/components/base/icons/line/arrows/chevron-down'
import CheckCircle from '@/app/components/base/icons/solid/general/check-circle'
import DataSetIcon from '@/app/components/base/icons/public/data-set'
import type { Emoji } from '@/types/tools'
import AppIcon from '@/app/components/base/app-icon'
type Props = {
payload: ToolInfoInThought
allToolIcons?: Record<string, string | Emoji>
}
const getIcon = (toolName: string, allToolIcons: Record<string, string | Emoji>) => {
if (toolName.startsWith('dataset-'))
return <DataSetIcon className='shrink-0'></DataSetIcon>
const icon = allToolIcons[toolName]
if (!icon)
return null
return (
typeof icon === 'string'
? (
<div
className='w-3 h-3 bg-cover bg-center rounded-[3px] shrink-0'
style={{
backgroundImage: `url(${icon})`,
}}
></div>
)
: (
<AppIcon
className='rounded-[3px] shrink-0'
size='xs'
icon={icon?.content}
background={icon?.background}
/>
))
}
const Tool: FC<Props> = ({
payload,
allToolIcons = {},
}) => {
const { t } = useTranslation()
const { name, input, isFinished, output } = payload
const toolName = name.startsWith('dataset-') ? t('dataset.knowledge') : name
const [isShowDetail, setIsShowDetail] = useState(false)
const icon = getIcon(toolName, allToolIcons) as any
return (
<div>
<div className={cn(!isShowDetail && 'shadow-sm', !isShowDetail && 'inline-block', 'max-w-full overflow-x-auto bg-white rounded-md')}>
<div
className={cn('flex items-center h-7 px-2 cursor-pointer')}
onClick={() => setIsShowDetail(!isShowDetail)}
>
{!isFinished && (
<Loading02 className='w-3 h-3 text-gray-500 animate-spin shrink-0' />
)}
{isFinished && !isShowDetail && (
<CheckCircle className='w-3 h-3 text-[#12B76A] shrink-0' />
)}
{isFinished && isShowDetail && (
icon
)}
<span className='mx-1 text-xs font-medium text-gray-500 shrink-0'>
{t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
</span>
<span
className='text-xs font-medium text-gray-700 truncate'
title={toolName}
>
{toolName}
</span>
<ChevronDown
className={cn(isShowDetail && 'rotate-180', 'ml-1 w-3 h-3 text-gray-500 select-none cursor-pointer shrink-0')}
/>
</div>
{isShowDetail && (
<div className='border-t border-black/5 p-2 space-y-2 '>
<Panel
isRequest={true}
toolName={toolName}
content={input} />
{output && (
<Panel
isRequest={false}
toolName={toolName}
content={output as string} />
)}
</div>
)}
</div>
</div>
)
}
export default React.memo(Tool)
This diff is collapsed.
import { useState } from 'react' import { useState } from 'react'
import produce from 'immer' import produce from 'immer'
import { useGetState } from 'ahooks'
import type { ConversationItem } from '@/types/app' import type { ConversationItem } from '@/types/app'
const storageConversationIdKey = 'conversationIdInfo' const storageConversationIdKey = 'conversationIdInfo'
...@@ -7,7 +8,7 @@ const storageConversationIdKey = 'conversationIdInfo' ...@@ -7,7 +8,7 @@ const storageConversationIdKey = 'conversationIdInfo'
type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'> type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>
function useConversation() { function useConversation() {
const [conversationList, setConversationList] = useState<ConversationItem[]>([]) const [conversationList, setConversationList] = useState<ConversationItem[]>([])
const [currConversationId, doSetCurrConversationId] = useState<string>('-1') const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1')
// when set conversation id, we do not have set appId // when set conversation id, we do not have set appId
const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
doSetCurrConversationId(id) doSetCurrConversationId(id)
...@@ -50,6 +51,7 @@ function useConversation() { ...@@ -50,6 +51,7 @@ function useConversation() {
conversationList, conversationList,
setConversationList, setConversationList,
currConversationId, currConversationId,
getCurrConversationId,
setCurrConversationId, setCurrConversationId,
getConversationIdFromStorage, getConversationIdFromStorage,
isNewConversation, isNewConversation,
......
...@@ -5,6 +5,8 @@ import commonEn from './lang/common.en' ...@@ -5,6 +5,8 @@ import commonEn from './lang/common.en'
import commonZh from './lang/common.zh' import commonZh from './lang/common.zh'
import appEn from './lang/app.en' import appEn from './lang/app.en'
import appZh from './lang/app.zh' import appZh from './lang/app.zh'
import toolsEn from './lang/tools.en'
import toolsZh from './lang/tools.zh'
import type { Locale } from '.' import type { Locale } from '.'
const resources = { const resources = {
...@@ -12,12 +14,16 @@ const resources = { ...@@ -12,12 +14,16 @@ const resources = {
translation: { translation: {
common: commonEn, common: commonEn,
app: appEn, app: appEn,
// tools
tools: toolsEn,
}, },
}, },
'zh-Hans': { 'zh-Hans': {
translation: { translation: {
common: commonZh, common: commonZh,
app: appZh, app: appZh,
// tools
tools: toolsZh,
}, },
}, },
} }
......
const translation = {
title: 'Tools',
createCustomTool: 'Create Custom Tool',
type: {
all: 'All',
builtIn: 'Built-in',
custom: 'Custom',
},
contribute: {
line1: 'I\'m interested in ',
line2: 'contributing tools to Dify.',
viewGuide: 'View the guide',
},
author: 'By',
auth: {
unauthorized: 'To Authorize',
authorized: 'Authorized',
setup: 'Set up authorization to use',
setupModalTitle: 'Set Up Authorization',
setupModalTitleDescription: 'After configuring credentials, all members within the workspace can use this tool when orchestrating applications.',
},
includeToolNum: '{{num}} tools included',
addTool: 'Add Tool',
createTool: {
title: 'Create Custom Tool',
editAction: 'Configure',
editTitle: 'Edit Custom Tool',
name: 'Name',
toolNamePlaceHolder: 'Enter the tool name',
schema: 'Schema',
schemaPlaceHolder: 'Enter your OpenAPI schema here',
viewSchemaSpec: 'View the OpenAPI-Swagger Specification',
importFromUrl: 'Import from URL',
importFromUrlPlaceHolder: 'https://...',
urlError: 'Please enter a valid URL',
examples: 'Examples',
exampleOptions: {
json: 'Weather(JSON)',
yaml: 'Pet Store(YAML)',
blankTemplate: 'Blank Template',
},
availableTools: {
title: 'Available Tools',
name: 'Name',
description: 'Description',
method: 'Method',
path: 'Path',
action: 'Actions',
test: 'Test',
},
authMethod: {
title: 'Authorization method',
type: 'Authorization type',
types: {
none: 'None',
api_key: 'API Key',
},
key: 'Key',
value: 'Value',
},
privacyPolicy: 'Privacy policy',
privacyPolicyPlaceholder: 'Please enter privacy policy',
},
test: {
title: 'Test',
parametersValue: 'Parameters & Value',
parameters: 'Parameters',
value: 'Value',
testResult: 'Test Results',
testResultPlaceholder: 'Test result will show here',
},
thought: {
using: 'Using',
used: 'Used',
requestTitle: 'Request to',
responseTitle: 'Response from',
},
setBuiltInTools: {
info: 'Info',
setting: 'Setting',
toolDescription: 'Tool description',
parameters: 'parameters',
string: 'string',
number: 'number',
required: 'Required',
infoAndSetting: 'Info & Settings',
},
noCustomTool: {
title: 'No custom tools!',
content: 'Add and manage your custom tools here for building AI apps.',
createTool: 'Create Tool',
},
noSearchRes: {
title: 'Sorry, no results!',
content: 'We couldn\'t find any tools that match your search.',
reset: 'Reset Search',
},
builtInPromptTitle: 'Prompt',
toolRemoved: 'Tool removed',
notAuthorized: 'Tool not authorized',
}
export default translation
const translation = {
title: '工具',
createCustomTool: '创建自定义工具',
type: {
all: '全部',
builtIn: '内置',
custom: '自定义',
},
contribute: {
line1: '我有兴趣为 ',
line2: 'Dify 贡献工具。',
viewGuide: '查看指南',
},
author: '作者',
auth: {
unauthorized: '去授权',
authorized: '已授权',
setup: '要使用请先授权',
setupModalTitle: '设置授权',
setupModalTitleDescription: '配置凭据后,工作区中的所有成员都可以在编排应用程序时使用此工具。',
},
includeToolNum: '包含 {{num}} 个工具',
addTool: '添加工具',
createTool: {
title: '创建自定义工具',
editAction: '编辑',
editTitle: '编辑自定义工具',
name: '名称',
toolNamePlaceHolder: '输入工具名称',
schema: 'Schema',
schemaPlaceHolder: '在此处输入您的 OpenAPI schema',
viewSchemaSpec: '查看 OpenAPI-Swagger 规范',
importFromUrl: '从 URL 中导入',
importFromUrlPlaceHolder: 'https://...',
urlError: '请输入有效的 URL',
examples: '例子',
exampleOptions: {
json: '天气(JSON)',
yaml: '宠物商店(YAML)',
blankTemplate: '空白模版',
},
availableTools: {
title: '可用工具',
name: '名称',
description: '描述',
method: '方法',
path: '路径',
action: '操作',
test: '测试',
},
authMethod: {
title: '鉴权方法',
type: '鉴权类型',
types: {
none: '无',
api_key: 'API Key',
},
key: '键',
value: '值',
},
privacyPolicy: '隐私协议',
privacyPolicyPlaceholder: '请输入隐私协议',
},
thought: {
using: '正在使用',
used: '已使用',
requestTitle: '请求来自',
responseTitle: '响应来自',
},
setBuiltInTools: {
info: '信息',
setting: '设置',
toolDescription: '工具描述',
parameters: '参数',
string: '字符串',
number: '数字',
required: '必填',
infoAndSetting: '信息和设置',
},
noCustomTool: {
title: '没有自定义工具!',
content: '在此统一添加和管理你的自定义工具,方便构建应用时使用。',
createTool: '创建工具',
},
noSearchRes: {
title: '抱歉,没有结果!',
content: '我们找不到任何与您的搜索相匹配的工具。',
reset: '重置搜索',
},
builtInPromptTitle: '提示词',
toolRemoved: '工具已被移除',
notAuthorized: '工具未授权',
}
export default translation
import { API_PREFIX } from '@/config' import { API_PREFIX } from '@/config'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/chat/type'
import type { VisionFile } from '@/types/app'
const TIME_OUT = 100000 const TIME_OUT = 100000
...@@ -21,20 +23,35 @@ const baseOptions = { ...@@ -21,20 +23,35 @@ const baseOptions = {
} }
export type IOnDataMoreInfo = { export type IOnDataMoreInfo = {
conversationId: string | undefined conversationId?: string
taskId?: string
messageId: string messageId: string
errorMessage?: string errorMessage?: string
errorCode?: string
} }
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
export type IOnCompleted = () => void export type IOnThought = (though: ThoughtItem) => void
export type IOnError = (msg: string) => void export type IOnFile = (file: VisionFile) => void
export type IOnMessageEnd = (messageEnd: MessageEnd) => void
export type IOnMessageReplace = (messageReplace: MessageReplace) => void
export type IOnAnnotationReply = (messageReplace: AnnotationReply) => void
export type IOnCompleted = (hasError?: boolean) => void
export type IOnError = (msg: string, code?: string) => void
type IOtherOptions = { type IOtherOptions = {
isPublicAPI?: boolean
bodyStringify?: boolean
needAllResponseContent?: boolean needAllResponseContent?: boolean
deleteContentType?: boolean
onData?: IOnData // for stream onData?: IOnData // for stream
onThought?: IOnThought
onFile?: IOnFile
onMessageEnd?: IOnMessageEnd
onMessageReplace?: IOnMessageReplace
onError?: IOnError onError?: IOnError
onCompleted?: IOnCompleted // for stream onCompleted?: IOnCompleted // for stream
getAbortController?: (abortController: AbortController) => void
} }
function unicodeToChar(text: string) { function unicodeToChar(text: string) {
...@@ -43,17 +60,18 @@ function unicodeToChar(text: string) { ...@@ -43,17 +60,18 @@ function unicodeToChar(text: string) {
}) })
} }
const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted) => { const handleStream = (response: Response, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought, onMessageEnd?: IOnMessageEnd, onMessageReplace?: IOnMessageReplace, onFile?: IOnFile) => {
if (!response.ok) if (!response.ok)
throw new Error('Network response was not ok') throw new Error('Network response was not ok')
const reader = response.body.getReader() const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')
let buffer = '' let buffer = ''
let bufferObj: any let bufferObj: Record<string, any>
let isFirstMessage = true let isFirstMessage = true
function read() { function read() {
reader.read().then((result: any) => { let hasError = false
reader?.read().then((result: any) => {
if (result.done) { if (result.done) {
onCompleted && onCompleted() onCompleted && onCompleted()
return return
...@@ -62,27 +80,51 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted ...@@ -62,27 +80,51 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
const lines = buffer.split('\n') const lines = buffer.split('\n')
try { try {
lines.forEach((message) => { lines.forEach((message) => {
if (!message || !message.startsWith('data: ')) if (message.startsWith('data: ')) { // check if it starts with data:
return try {
try { bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json
bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json }
} catch (e) {
catch (e) { // mute handle message cut off
// mute handle message cut off onData('', isFirstMessage, {
onData('', isFirstMessage, { conversationId: bufferObj?.conversation_id,
conversationId: bufferObj?.conversation_id, messageId: bufferObj?.message_id,
messageId: bufferObj?.id, })
}) return
return }
if (bufferObj.status === 400 || !bufferObj.event) {
onData('', false, {
conversationId: undefined,
messageId: '',
errorMessage: bufferObj?.message,
errorCode: bufferObj?.code,
})
hasError = true
onCompleted?.(true)
return
}
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
// can not use format here. Because message is splited.
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
conversationId: bufferObj.conversation_id,
taskId: bufferObj.task_id,
messageId: bufferObj.id,
})
isFirstMessage = false
}
else if (bufferObj.event === 'agent_thought') {
onThought?.(bufferObj as ThoughtItem)
}
else if (bufferObj.event === 'message_file') {
onFile?.(bufferObj as VisionFile)
}
else if (bufferObj.event === 'message_end') {
onMessageEnd?.(bufferObj as MessageEnd)
}
else if (bufferObj.event === 'message_replace') {
onMessageReplace?.(bufferObj as MessageReplace)
}
} }
if (bufferObj.event !== 'message')
return
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id,
})
isFirstMessage = false
}) })
buffer = lines[lines.length - 1] buffer = lines[lines.length - 1]
} }
...@@ -92,10 +134,12 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted ...@@ -92,10 +134,12 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
messageId: '', messageId: '',
errorMessage: `${e}`, errorMessage: `${e}`,
}) })
hasError = true
onCompleted?.(true)
return return
} }
if (!hasError)
read() read()
}) })
} }
read() read()
...@@ -214,7 +258,7 @@ export const upload = (fetchOptions: any): Promise<any> => { ...@@ -214,7 +258,7 @@ export const upload = (fetchOptions: any): Promise<any> => {
}) })
} }
export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onError }: IOtherOptions) => { export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, onThought, onFile, onMessageEnd, onMessageReplace, onError }: IOtherOptions) => {
const options = Object.assign({}, baseOptions, { const options = Object.assign({}, baseOptions, {
method: 'POST', method: 'POST',
}, fetchOptions) }, fetchOptions)
...@@ -246,7 +290,7 @@ export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, o ...@@ -246,7 +290,7 @@ export const ssePost = (url: string, fetchOptions: any, { onData, onCompleted, o
onData?.(str, isFirstMessage, moreInfo) onData?.(str, isFirstMessage, moreInfo)
}, () => { }, () => {
onCompleted?.() onCompleted?.()
}) }, onThought, onMessageEnd, onMessageReplace, onFile)
}).catch((e) => { }).catch((e) => {
Toast.notify({ type: 'error', message: e }) Toast.notify({ type: 'error', message: e })
onError?.(e) onError?.(e)
......
import type { IOnCompleted, IOnData, IOnError } from './base' import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base'
import { get, post, ssePost } from './base' import { get, post, ssePost } from './base'
import type { Feedbacktype } from '@/types/app' import type { Feedbacktype } from '@/types/app'
export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError }: { export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: {
onData: IOnData onData: IOnData
onCompleted: IOnCompleted onCompleted: IOnCompleted
onFile: IOnFile
onThought: IOnThought
onMessageEnd: IOnMessageEnd
onMessageReplace: IOnMessageReplace
onError: IOnError onError: IOnError
getAbortController?: (abortController: AbortController) => void
}) => { }) => {
return ssePost('chat-messages', { return ssePost('chat-messages', {
body: { body: {
...body, ...body,
response_mode: 'streaming', response_mode: 'streaming',
}, },
}, { onData, onCompleted, onError }) }, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace })
} }
export const fetchConversations = async () => { export const fetchConversations = async () => {
return get('conversations', { params: { limit: 20, first_id: '' } }) return get('conversations', { params: { limit: 100, first_id: '' } })
} }
export const fetchChatList = async (conversationId: string) => { export const fetchChatList = async (conversationId: string) => {
...@@ -31,3 +36,7 @@ export const fetchAppParams = async () => { ...@@ -31,3 +36,7 @@ export const fetchAppParams = async () => {
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => { export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
return post(url, { body }) return post(url, { body })
} }
export const generationConversationName = async (id: string) => {
return post(`conversations/${id}/name`, { body: { auto_generate: true } })
}
import type { Annotation } from './log'
import type { Locale } from '@/i18n' import type { Locale } from '@/i18n'
import type { ThoughtItem } from '@/app/components/chat/type'
export type PromptVariable = { export type PromptVariable = {
key: string key: string
...@@ -74,9 +76,12 @@ export type IChatItem = { ...@@ -74,9 +76,12 @@ export type IChatItem = {
* More information about this message * More information about this message
*/ */
more?: MessageMore more?: MessageMore
isIntroduction?: boolean annotation?: Annotation
useCurrentUserAvatar?: boolean useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean isOpeningStatement?: boolean
suggestedQuestions?: string[]
log?: { role: string; text: string }[]
agent_thoughts?: ThoughtItem[]
message_files?: VisionFile[] message_files?: VisionFile[]
} }
...@@ -133,4 +138,5 @@ export type VisionFile = { ...@@ -133,4 +138,5 @@ export type VisionFile = {
transfer_method: TransferMethod transfer_method: TransferMethod
url: string url: string
upload_file_id: string upload_file_id: string
belongs_to?: string
} }
export type TypeWithI18N<T = string> = {
'en_US': T
'zh_Hans': T
[key: string]: T
}
export type LogAnnotation = {
content: string
account: {
id: string
name: string
email: string
}
created_at: number
}
export type Annotation = {
id: string
authorName: string
logAnnotation?: LogAnnotation
created_at?: number
}
import type { TypeWithI18N } from './base'
export enum LOC {
tools = 'tools',
app = 'app',
}
export enum AuthType {
none = 'none',
apiKey = 'api_key',
}
export type Credential = {
'auth_type': AuthType
'api_key_header'?: string
'api_key_value'?: string
}
export enum CollectionType {
all = 'all',
builtIn = 'builtin',
custom = 'api',
}
export type Emoji = {
background: string
content: string
}
export type Collection = {
id: string
name: string
author: string
description: TypeWithI18N
icon: string | Emoji
label: TypeWithI18N
type: CollectionType
team_credentials: Record<string, any>
is_team_authorization: boolean
allow_delete: boolean
}
export type ToolParameter = {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
type: string
required: boolean
default: string
options?: {
label: TypeWithI18N
value: string
}[]
}
export type Tool = {
name: string
label: TypeWithI18N
description: any
parameters: ToolParameter[]
}
export type ToolCredential = {
name: string
label: TypeWithI18N
help: TypeWithI18N
placeholder: TypeWithI18N
type: string
required: boolean
default: string
options?: {
label: TypeWithI18N
value: string
}[]
}
export type CustomCollectionBackend = {
provider: string
original_provider?: string
credentials: Credential
icon: Emoji
schema_type: string
schema: string
privacy_policy: string
tools?: ParamItem[]
}
export type ParamItem = {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
type: string
required: boolean
default: string
min?: number
max?: number
options?: {
label: TypeWithI18N
value: string
}[]
}
export type CustomParamSchema = {
operation_id: string // name
summary: string
server_url: string
method: string
parameters: ParamItem[]
}
import type { ThoughtItem } from '@/app/components/chat/type'
import type { VisionFile } from '@/types/app'
export const sortAgentSorts = (list: ThoughtItem[]) => {
if (!list)
return list
if (list.some(item => item.position === undefined))
return list
const temp = [...list]
temp.sort((a, b) => a.position - b.position)
return temp
}
export const addFileInfos = (list: ThoughtItem[], messageFiles: VisionFile[]) => {
if (!list || !messageFiles)
return list
return list.map((item) => {
if (item.files && item.files?.length > 0) {
return {
...item,
message_files: item.files.map(fileId => messageFiles.find(file => file.id === fileId)) as VisionFile[],
}
}
return item
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment