From e4784bcd60681dfcd96b24bff2e68c7d6627f4f0 Mon Sep 17 00:00:00 2001 From: Paul Ccari <46382556+paulclindo@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:00:52 -0500 Subject: [PATCH 01/13] feat: python support + add configs in tool creation (#560) * fix * fix: update search response * feat: python support * feat: edit tool python * fix types * refactor * refactor * refactor and add python tool details page * hide python execution * fix * add configs * feat: remove agents from experimental feature * feat: add config --- apps/shinkai-desktop/package.json | 2 +- .../src/components/agent/add-agent.tsx | 2 +- .../components/language-tool-selector.tsx | 160 +++ .../components/tool-playground.tsx | 811 +++++++++++ .../components/tools-selection.tsx | 208 +++ .../playground-tool/hooks/use-tool-code.ts | 459 ++++++- .../hooks/use-tool-metadata.ts | 77 +- .../components/playground-tool/utils/code.ts | 36 + .../src/components/tools/python-tool.tsx | 262 ++++ .../src/components/tools/tool-details.tsx | 12 + apps/shinkai-desktop/src/pages/ais.tsx | 30 +- .../shinkai-desktop/src/pages/create-tool.tsx | 1192 +---------------- apps/shinkai-desktop/src/pages/edit-tool.tsx | 842 +----------- apps/shinkai-desktop/src/pages/tools.tsx | 2 +- .../shinkai-message-ts/src/api/tools/index.ts | 5 +- .../shinkai-message-ts/src/api/tools/types.ts | 21 +- .../src/v2/mutations/createToolCode/index.ts | 3 +- .../src/v2/mutations/createToolCode/types.ts | 6 +- .../src/v2/mutations/executeToolCode/index.ts | 17 +- .../src/v2/mutations/executeToolCode/types.ts | 7 +- .../src/v2/mutations/saveToolCode/index.ts | 2 + .../src/v2/mutations/saveToolCode/types.ts | 6 +- .../src/components/chat/message-list.tsx | 3 + .../src/components/chat/message.tsx | 10 +- .../rjsf/FieldTemplate/FieldTemplate.tsx | 2 +- .../components/rjsf/TitleField/TitleField.tsx | 6 +- 26 files changed, 2064 insertions(+), 2119 deletions(-) create mode 100644 apps/shinkai-desktop/src/components/playground-tool/components/language-tool-selector.tsx create mode 100644 apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx create mode 100644 apps/shinkai-desktop/src/components/playground-tool/components/tools-selection.tsx create mode 100644 apps/shinkai-desktop/src/components/playground-tool/utils/code.ts create mode 100644 apps/shinkai-desktop/src/components/tools/python-tool.tsx diff --git a/apps/shinkai-desktop/package.json b/apps/shinkai-desktop/package.json index fc6fef9cc..3764f407e 100644 --- a/apps/shinkai-desktop/package.json +++ b/apps/shinkai-desktop/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && NODE_OPTIONS=\\\"--max-old-space-size=8192\\\" vite build ", + "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint --ext .js,.jsx,.ts,.tsx . --ignore-path .gitignore", "tauri": "tauri", diff --git a/apps/shinkai-desktop/src/components/agent/add-agent.tsx b/apps/shinkai-desktop/src/components/agent/add-agent.tsx index 534c951b5..64cf57fac 100644 --- a/apps/shinkai-desktop/src/components/agent/add-agent.tsx +++ b/apps/shinkai-desktop/src/components/agent/add-agent.tsx @@ -217,7 +217,7 @@ function AddAgentPage() { /> - + {llmProviders?.length && llmProviders.map((llmProvider) => ( + + + + + + ), + }, + { + label: CodeLanguage.Typescript, + value: CodeLanguage.Typescript, + icon: ( + + + + + + ), + }, +]; + +export function LanguageToolSelector({ + value, + onValueChange, +}: { + value: string; + onValueChange: (value: string) => void; +}) { + return ( + + + + + .icon]:rotate-180', + )} + > +
+ { + ( + LANGUAGE_TOOLS.find( + (language) => language.value === value, + ) ?? LANGUAGE_TOOLS[0] + ).icon + } + {value} +
+ +
+
+ + + Switch code language + + + + + {LANGUAGE_TOOLS.map((language) => ( + +
+ {language.icon} +
+ {language.label} +
+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx b/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx new file mode 100644 index 000000000..c6542af81 --- /dev/null +++ b/apps/shinkai-desktop/src/components/playground-tool/components/tool-playground.tsx @@ -0,0 +1,811 @@ +import { ReloadIcon } from '@radix-ui/react-icons'; +import { FormProps } from '@rjsf/core'; +import validator from '@rjsf/validator-ajv8'; +import { useTranslation } from '@shinkai_network/shinkai-i18n'; +import { + CodeLanguage, + ToolMetadata, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { + Badge, + Button, + ChatInputArea, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + JsonForm, + MessageList, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from '@shinkai_network/shinkai-ui'; +import { SendIcon } from '@shinkai_network/shinkai-ui/assets'; +import { cn } from '@shinkai_network/shinkai-ui/utils'; +import { AnimatePresence, motion } from 'framer-motion'; +import { + ArrowUpRight, + Loader2, + LucideArrowLeft, + Play, + Redo2Icon, + Save, + Undo2Icon, +} from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Link, To } from 'react-router-dom'; + +import { useAuth } from '../../../store/auth'; +import { AIModelSelector } from '../../chat/chat-action-bar/ai-update-selection-action-bar'; +import { ToolErrorFallback } from '../error-boundary'; +import { + CreateToolCodeFormSchema, + useToolCode, + useToolForm, +} from '../hooks/use-tool-code'; +import { useToolMetadata } from '../hooks/use-tool-metadata'; +import PlaygroundToolLayout from '../layout'; +import ToolCodeEditor from '../tool-code-editor'; +import { detectLanguage } from '../utils/code'; +import { LanguageToolSelector } from './language-tool-selector'; +import { ToolsSelection } from './tools-selection'; + +function PlaygroundToolEditor({ + mode, + createToolCodeFormInitialValues, + toolCodeInitialValues, + toolMetadataInitialValues, + initialChatInboxId, + toolName, +}: { + mode: 'create' | 'edit'; + createToolCodeFormInitialValues?: Partial; + toolMetadataInitialValues?: { + metadata: ToolMetadata | null; + state?: 'idle' | 'pending' | 'success' | 'error'; + error?: string | null; + }; + toolCodeInitialValues?: { + code: string; + state?: 'idle' | 'pending' | 'success' | 'error'; + error?: string | null; + }; + toolName?: string; + initialChatInboxId?: string; +}) { + const auth = useAuth((state) => state.auth); + const { t } = useTranslation(); + const toolResultBoxRef = useRef(null); + const [formData, setFormData] = useState(null); + + const form = useToolForm(createToolCodeFormInitialValues); + + useEffect(() => { + if (createToolCodeFormInitialValues?.language) { + form.setValue('language', createToolCodeFormInitialValues.language); + } + }, [form]); + + const { + chatInboxId, + toolCode, + baseToolCodeRef, + isToolCodeGenerationPending, + isToolCodeGenerationSuccess, + fetchPreviousPage, + hasPreviousPage, + isChatConversationLoading, + isFetchingPreviousPage, + isChatConversationSuccess, + chatConversationData, + toolHistory, + codeEditorRef, + metadataEditorRef, + executeToolCodeQuery, + toolResult, + isDirtyCodeEditor, + setIsDirtyCodeEditor, + forceGenerateMetadata, + resetCounter, + restoreCode, + handleApplyChangesCodeSubmit, + isRestoringToolConversation, + goPreviousToolCode, + goNextToolCode, + isSavingTool, + handleSaveTool, + resetToolCode, + handleCreateToolCode, + } = useToolCode({ + createToolCodeForm: form, + initialState: toolCodeInitialValues, + initialChatInboxId, + }); + + const { + isMetadataGenerationPending, + isMetadataGenerationSuccess, + isMetadataGenerationIdle, + metadataGenerationData, + metadataGenerationError, + isMetadataGenerationError, + regenerateToolMetadata, + } = useToolMetadata({ + chatInboxId, + toolCode, + tools: form.watch('tools'), + forceGenerateMetadata, + initialState: toolMetadataInitialValues, + }); + + const handleRunCode: FormProps['onSubmit'] = async (data) => { + const { configs, params } = data.formData; + const updatedCodeWithoutSave = codeEditorRef.current?.value ?? ''; + await executeToolCodeQuery.mutateAsync({ + code: isDirtyCodeEditor ? updatedCodeWithoutSave : toolCode, + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + params, + llmProviderId: form.getValues('llmProviderId'), + tools: form.getValues('tools'), + language: form.getValues('language'), + configs, + }); + }; + + return ( + +
+ + + {t('common.back')} + +

+ {mode === 'create' ? 'Tool Playground' : `Edit ${toolName}`} +

+
+
+ {!chatInboxId && ( + <> + + 🤖 + +

+ Generate your tool using AI +

+

+ Ask Shinkai AI to generate a tool for you. Provide a prompt + and Shinkai AI will generate a tool code for you. +

+
+ {[ + { + text: 'Tool for downloading a website content in markdown', + prompt: + 'Generate a tool for downloading a website into markdown', + }, + { + text: 'Tool for getting tech-related stories from Hacker News', + prompt: + 'Generate a tool for getting top tech-related stories from Hacker News, include the title, author, and URL of the story', + }, + ].map((suggestion) => ( + + form.setValue('message', suggestion.prompt) + } + variant="outline" + > + {suggestion.text} + + + ))} +
+ + )} + + {chatInboxId && ( + + )} +
+ +
+ +
+ ( + + + {t('chat.enterMessage')} + + +
+
+ { + form.setValue('llmProviderId', value); + }} + value={form.watch('llmProviderId')} + /> + { + form.setValue( + 'language', + value as CodeLanguage, + ); + }} + value={form.watch('language')} + /> + +
+ + + Enter to + send + + +
+ } + disabled={ + isToolCodeGenerationPending || + isMetadataGenerationPending + } + onChange={field.onChange} + onSubmit={form.handleSubmit(handleCreateToolCode)} + topAddons={<>} + value={field.value} + /> +
+ + + )} + /> + +
+ + + } + rightElement={ + +
+
+
+ + + Code + + + Metadata + + +
+ {toolHistory.length > 1 && ( +
+ {toolCode === toolHistory?.at(-1)?.code ? ( + + + + Latest + + + + +

This is your latest version

+
+
+
+ ) : ( + + + + {isRestoringToolConversation ? ( + + ) : null} + Restore + + + + +

+ Restore to this version. This action will undo + all changes made since the selected version +

+
+
+
+ )} +
+ + + + + + +

View previous version

+
+
+
+ + + + + + +

View next version

+
+
+
+
+
+ )} + +
+
+ + + +
+
+
+
+

+ Code{' '} +

+ {toolCode && ( +

+ {/* eslint-disable-next-line react/no-unescaped-entities */} + Here's the code generated by Shinkai AI based on + your prompt. +

+ )} +
+ + {/*{toolCode && (*/} + {/* */} + {/* */} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/* */} + {/* */} + {/*

Copy Code

*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*)}*/} +
+
+ {isToolCodeGenerationPending && ( +
+ + Generating Code... +
+ )} + {!isToolCodeGenerationPending && + !toolCode && + !isToolCodeGenerationSuccess && ( +

+ No code generated yet.
+ Ask Shinkai AI to generate your tool code. +

+ )} + {isToolCodeGenerationSuccess && toolCode && ( +
+
+ + {' '} + {detectLanguage(toolCode)}{' '} + {isDirtyCodeEditor && ( + + )} + + + {isDirtyCodeEditor && ( + + + + + )} + +
+ { + setIsDirtyCodeEditor( + currentCode !== baseToolCodeRef.current, + ); + }} + ref={codeEditorRef} + value={toolCode} + /> + + )} +
+
+
+
+ + + +
+
+
+

+ Run +

+ {metadataGenerationData && ( +

Fill in the options above to run your tool.

+ )} +
+ {isMetadataGenerationSuccess && + !isToolCodeGenerationPending && + !isMetadataGenerationError && ( + + )} +
+
+ {(isMetadataGenerationPending || + isToolCodeGenerationPending) && ( +
+ + Generating... +
+ )} + {!isMetadataGenerationPending && + !isToolCodeGenerationPending && + isMetadataGenerationError && ( + + )} + {isMetadataGenerationSuccess && + !isToolCodeGenerationPending && + !isMetadataGenerationError && ( +
+ setFormData(e.formData)} + onSubmit={handleRunCode} + schema={{ + type: 'object', + properties: { + ...(metadataGenerationData?.configurations + ?.properties && + Object.keys( + metadataGenerationData.configurations + .properties, + ).length > 0 + ? { + configs: + metadataGenerationData.configurations, + } + : {}), + ...(metadataGenerationData?.parameters + ?.properties && + Object.keys( + metadataGenerationData.parameters + .properties, + ).length > 0 + ? { + params: + metadataGenerationData.parameters, + } + : {}), + }, + }} + uiSchema={{ + 'ui:submitButtonOptions': { norender: true }, + configs: { + 'ui:title': 'Config', + }, + params: { + 'ui:title': 'Inputs', + }, + }} + validator={validator} + /> + + {(executeToolCodeQuery.isPending || + executeToolCodeQuery.isError || + executeToolCodeQuery.isSuccess) && ( + + {executeToolCodeQuery.isPending && ( +
+ + Running Tool... +
+ )} + {executeToolCodeQuery.isError && ( +
+

+ Tool execution failed. Try generating + the tool code again. +

+
+                                          {executeToolCodeQuery.error?.response
+                                            ?.data?.message ??
+                                            executeToolCodeQuery.error?.message}
+                                        
+
+ )} +
+ {executeToolCodeQuery.isSuccess && + toolResult && ( + + )} +
+
+ )} +
+
+ )} + {isMetadataGenerationIdle && + !isToolCodeGenerationPending && ( +
+

+ No metadata generated yet. +

+
+ )} +
+
+
+
+
+ +
+
+
+

+ Metadata +

+ {metadataGenerationData && ( +

Fill in the options above to run your tool.

+ )} +
+ {isMetadataGenerationSuccess && ( + + )} +
+ {isMetadataGenerationPending && ( +
+ + Generating Metadata... +
+ )} + {!isMetadataGenerationPending && + !isToolCodeGenerationPending && + isMetadataGenerationError && ( + + )} + + {isMetadataGenerationSuccess && + !isMetadataGenerationError && ( +
+
+ +
+
+ )} + {isMetadataGenerationIdle && ( +
+

+ No metadata generated yet. +

+
+ )} +
+
+
+
+
+ } + /> + ); +} + +export default PlaygroundToolEditor; diff --git a/apps/shinkai-desktop/src/components/playground-tool/components/tools-selection.tsx b/apps/shinkai-desktop/src/components/playground-tool/components/tools-selection.tsx new file mode 100644 index 000000000..694fac0ff --- /dev/null +++ b/apps/shinkai-desktop/src/components/playground-tool/components/tools-selection.tsx @@ -0,0 +1,208 @@ +import { useGetTools } from '@shinkai_network/shinkai-node-state/v2/queries/getToolsList/useGetToolsList'; +import { + Badge, + FormControl, + FormField, + FormItem, + Popover, + PopoverContent, + PopoverTrigger, + Switch, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipProvider, + TooltipTrigger, +} from '@shinkai_network/shinkai-ui'; +import { formatText } from '@shinkai_network/shinkai-ui/helpers'; +import { cn } from '@shinkai_network/shinkai-ui/utils'; +import { BoltIcon } from 'lucide-react'; +import { InfoCircleIcon } from 'primereact/icons/infocircle'; +import { UseFormReturn } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import { toast } from 'sonner'; + +import { CreateToolCodeFormSchema } from '../../../pages/create-tool'; +import { useAuth } from '../../../store/auth'; +import { actionButtonClassnames } from '../../chat/conversation-footer'; + +export function ToolsSelection({ + form, +}: { + form: UseFormReturn; +}) { + const auth = useAuth((state) => state.auth); + + const { data: toolsList } = useGetTools({ + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + + return ( + + + +
+ Tools{' '} + + {form.watch('tools').length} + +
+
+ +
+

Available tools

+
+ { + const isAllConfigFilled = toolsList + ?.map((tool) => tool.config) + .filter((item) => !!item) + .flat() + ?.map((conf) => ({ + key_name: conf.BasicConfig.key_name, + key_value: conf.BasicConfig.key_value ?? '', + required: conf.BasicConfig.required, + })) + .every( + (conf) => + !conf.required || + (conf.required && conf.key_value !== ''), + ); + if (!isAllConfigFilled) { + toast.error('Tool configuration', { + description: + 'Please fill in the config required in tool details', + }); + return; + } + + if (checked && toolsList) { + form.setValue( + 'tools', + toolsList.map((tool) => tool.tool_router_key), + ); + } else { + form.setValue('tools', []); + } + }} + /> + +
+
+ +
+ {toolsList?.map((tool) => ( + ( + + +
+ { + const configs = tool?.config ?? []; + if ( + configs + .map((conf) => ({ + key_name: conf.BasicConfig.key_name, + key_value: conf.BasicConfig.key_value ?? '', + required: conf.BasicConfig.required, + })) + .every( + (conf) => + !conf.required || + (conf.required && conf.key_value !== ''), + ) + ) { + field.onChange( + field.value.includes(tool.tool_router_key) + ? field.value.filter( + (value) => value !== tool.tool_router_key, + ) + : [...field.value, tool.tool_router_key], + ); + + return; + } + toast.error('Tool configuration is required', { + description: + 'Please fill in the config required in tool details', + }); + }} + /> +
+ + + + + + + + {tool.description} + + + +
+ {(tool.config ?? []).length > 0 && ( + + + + + + + + +

Configure tool

+
+
+
+ )} +
+
+
+ )} + /> + ))} +
+
+
+
+ ); +} diff --git a/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-code.ts b/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-code.ts index c8ffb2a3a..ed41eafcc 100644 --- a/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-code.ts +++ b/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-code.ts @@ -1,12 +1,72 @@ -import {ChatConversationInfiniteData} from "@shinkai_network/shinkai-node-state/v2/queries/getChatConversation/types"; -import {useMemo, useRef, useState} from "react"; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + CodeLanguage, + ToolMetadata, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { extractJobIdFromInbox } from '@shinkai_network/shinkai-message-ts/utils/inbox_name_handler'; +import { useCreateToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolCode/useCreateToolCode'; +import { useExecuteToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/executeToolCode/useExecuteToolCode'; +import { useRestoreToolConversation } from '@shinkai_network/shinkai-node-state/v2/mutations/restoreToolConversation/useRestoreToolConversation'; +import { useSaveToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/saveToolCode/useSaveToolCode'; +import { useUpdateToolCodeImplementation } from '@shinkai_network/shinkai-node-state/v2/mutations/updateToolCodeImplementation/useUpdateToolCodeImplementation'; +import { PrismEditor } from 'prism-react-editor'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useForm, UseFormReturn } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { z } from 'zod'; -import {useChatConversationWithOptimisticUpdates} from "../../../pages/chat/chat-conversation"; -import {extractTypeScriptCode} from "../../../pages/create-tool"; +import { useChatConversationWithOptimisticUpdates } from '../../../pages/chat/chat-conversation'; +import { useAuth } from '../../../store/auth'; +import { useSettings } from '../../../store/settings'; +import { ToolMetadataSchema } from '../schemas'; +import { extractTypeScriptCode } from '../utils/code'; -export const useToolCode = ({ chatInboxId }: { chatInboxId?: string }) => { - const [toolCode, setToolCode] = useState(''); - const baseToolCodeRef = useRef(''); +export const createToolCodeFormSchema = z.object({ + message: z.string().min(1), + llmProviderId: z.string().min(1), + tools: z.array(z.string()), + language: z.nativeEnum(CodeLanguage), +}); + +export type CreateToolCodeFormSchema = z.infer; + +export const useToolForm = ( + initialValues?: Partial, +) => { + const defaultAgentId = useSettings( + (settingsStore) => settingsStore.defaultAgentId, + ); + + const form = useForm({ + resolver: zodResolver(createToolCodeFormSchema), + defaultValues: { + message: '', + tools: [], + language: CodeLanguage.Typescript, + ...initialValues, + }, + }); + + useEffect(() => { + if (defaultAgentId) { + form.setValue('llmProviderId', defaultAgentId); + } + }, [form, defaultAgentId]); + + return form; +}; + +const useChatConversation = (initialInboxId?: string) => { + const [chatInboxId, setChatInboxId] = useState( + initialInboxId, + ); + + useEffect(() => { + if (initialInboxId) { + setChatInboxId(initialInboxId); + } + }, [initialInboxId]); const { data, @@ -20,30 +80,77 @@ export const useToolCode = ({ chatInboxId }: { chatInboxId?: string }) => { forceRefetchInterval: true, }); - const chatConversationData: ChatConversationInfiniteData | undefined = - useMemo(() => { - if (!data) return; - const formattedData = data?.pages?.map((page) => { - return page.map((message) => { - return { - ...message, - content: - message.role === 'user' - ? message.content - .match(/([\s\S]*?)<\/input_command>/)?.[1] - ?.trim() ?? '' - : message.content, - }; - }); - }); - return { - ...data, - pages: formattedData, - }; - }, [data]); + const formattedConversationData = useMemo(() => { + if (!data) return; + return { + ...data, + pages: data.pages?.map((page) => + page.map((message) => ({ + ...message, + content: + message.role === 'user' + ? message.content + .match(/([\s\S]*?)<\/input_command>/)?.[1] + ?.trim() ?? '' + : message.content, + })), + ), + }; + }, [data]); + + return { + chatInboxId, + setChatInboxId, + conversationData: formattedConversationData, + fetchPreviousPage, + hasPreviousPage, + isChatConversationLoading, + isFetchingPreviousPage, + isChatConversationSuccess, + }; +}; + +export const useToolCode = ({ + initialChatInboxId, + initialState, + createToolCodeForm, +}: { + initialChatInboxId?: string; + initialState?: { + code: string; + state?: 'idle' | 'pending' | 'success' | 'error'; + error?: string | null; + }; + createToolCodeForm: UseFormReturn; +}) => { + const { + chatInboxId, + setChatInboxId, + conversationData, + fetchPreviousPage, + hasPreviousPage, + isChatConversationLoading, + isFetchingPreviousPage, + isChatConversationSuccess, + } = useChatConversation(initialChatInboxId); + + const auth = useAuth((state) => state.auth); + const navigate = useNavigate(); + + const baseToolCodeRef = useRef(''); + const codeEditorRef = useRef(null); + const metadataEditorRef = useRef(null); + const forceGenerateCode = useRef(false); + const forceGenerateMetadata = useRef(false); + + const [isDirtyCodeEditor, setIsDirtyCodeEditor] = useState(false); + const [resetCounter, setResetCounter] = useState(0); + const [toolResult, setToolResult] = useState(null); + + const currentLanguage = createToolCodeForm.watch('language'); const toolHistory = useMemo(() => { - const messageList = chatConversationData?.pages.flat() ?? []; + const messageList = conversationData?.pages.flat() ?? []; const toolCodesFound = messageList .map((message) => { if ( @@ -52,64 +159,259 @@ export const useToolCode = ({ chatInboxId }: { chatInboxId?: string }) => { ) { return { messageId: message.messageId, - code: extractTypeScriptCode(message.content) ?? '', + code: extractTypeScriptCode(message.content, currentLanguage) ?? '', }; } }) - .filter((item) => !!item); + .filter((item) => !!item) + .filter((item) => item.code); return toolCodesFound; - }, [chatConversationData?.pages]); + }, [conversationData?.pages, currentLanguage]); + + const [toolCode, setToolCode] = useState(initialState?.code ?? ''); + const [error, setError] = useState( + initialState?.error ?? null, + ); + const [toolCodeState, setToolCodeState] = useState< + 'idle' | 'pending' | 'success' | 'error' + >(initialState?.state ?? 'idle'); + + useEffect(() => { + if (initialState?.code) { + baseToolCodeRef.current = initialState.code; + forceGenerateCode.current = false; + setToolCode(initialState.code); + } + if (initialState?.state) { + setToolCodeState(initialState.state); + } + }, [initialState?.code, initialState?.state]); + + useEffect(() => { + const lastMessage = conversationData?.pages?.at(-1)?.at(-1); + if (!lastMessage || !forceGenerateCode.current) return; - const { - isToolCodeGenerationPending, - isToolCodeGenerationSuccess, - isToolCodeGenerationError, - isToolCodeGenerationIdle, - toolCodeGenerationData, - } = useMemo(() => { - const lastMessage = data?.pages?.at(-1)?.at(-1); - if (!lastMessage) - return { - isToolCodeGenerationPending: false, - isToolCodeGenerationSuccess: false, - isToolCodeGenerationIdle: false, - toolCodeGenerationData: '', - isToolCodeGenerationError: true, - }; - - let status: 'idle' | 'error' | 'success' | 'pending' = 'idle'; - let toolCodeGeneration = ''; if ( - lastMessage.role === 'assistant' && - lastMessage.status.type === 'running' + lastMessage?.role === 'assistant' && + lastMessage?.status.type === 'running' ) { - status = 'pending'; + setToolCodeState('pending'); + return; } if ( - lastMessage.role === 'assistant' && - lastMessage.status.type === 'complete' + lastMessage?.role === 'assistant' && + lastMessage?.status.type === 'complete' ) { - status = 'success'; - const generatedCode = extractTypeScriptCode(lastMessage?.content) ?? ''; + const generatedCode = + extractTypeScriptCode(lastMessage?.content, currentLanguage) ?? ''; if (generatedCode) { baseToolCodeRef.current = generatedCode; - toolCodeGeneration = generatedCode; setToolCode(generatedCode); + setToolCodeState('success'); } else { - status = 'error'; + setError('Failed to generate tool code'); + setToolCodeState('error'); } } + }, [conversationData?.pages, currentLanguage]); - return { - isToolCodeGenerationPending: status === 'pending', - isToolCodeGenerationSuccess: status === 'success', - isToolCodeGenerationIdle: status === 'idle', - toolCodeGenerationData: toolCodeGeneration, - isToolCodeGenerationError: status === 'error', - }; - }, [data?.pages]); + const isToolCodeGenerationPending = toolCodeState === 'pending'; + const isToolCodeGenerationSuccess = toolCodeState === 'success'; + const isToolCodeGenerationIdle = toolCodeState === 'idle'; + const isToolCodeGenerationError = toolCodeState === 'error'; + const toolCodeGenerationData = toolCode; + const toolCodeGenerationError = error; + + const executeToolCodeQuery = useExecuteToolCode({ + onSuccess: (data) => { + setToolResult(data); + }, + }); + + const { mutateAsync: createToolCode } = useCreateToolCode({ + onSuccess: (data) => { + setChatInboxId(data.inbox); + setToolCode(''); + forceGenerateCode.current = true; + forceGenerateMetadata.current = true; + baseToolCodeRef.current = ''; + setToolResult(null); + executeToolCodeQuery.reset(); + }, + }); + + const { mutateAsync: updateToolCodeImplementation } = + useUpdateToolCodeImplementation(); + + const { mutateAsync: saveToolCode, isPending: isSavingTool } = + useSaveToolCode({ + onSuccess: (data) => { + toast.success('Tool code saved successfully'); + navigate(`/tools/${data.metadata.tool_router_key}`); + }, + onError: (error) => { + toast.error('Failed to save tool code', { + position: 'top-right', + description: error.response?.data?.message ?? error.message, + }); + }, + }); + + const { + mutateAsync: restoreToolConversation, + isPending: isRestoringToolConversation, + } = useRestoreToolConversation({ + onSuccess: () => { + toast.success('Successfully restore changes'); + }, + onError: (error) => { + toast.error('Failed to restore tool conversation', { + position: 'top-right', + description: error.response?.data?.message ?? error.message, + }); + }, + }); + + const handleCreateToolCode = async (data: CreateToolCodeFormSchema) => { + if (!auth) return; + + await createToolCode({ + nodeAddress: auth.node_address, + token: auth.api_v2_key, + message: data.message, + llmProviderId: data.llmProviderId, + jobId: chatInboxId ? extractJobIdFromInbox(chatInboxId ?? '') : undefined, + tools: data.tools, + language: data.language, + }); + + createToolCodeForm.setValue('message', ''); + }; + + const handleSaveTool = async () => { + if (!chatInboxId) return; + + const metadataCode = metadataEditorRef.current?.value; + const toolCode = codeEditorRef.current?.value; + + let parsedMetadata: ToolMetadata; + + try { + const parseResult = JSON.parse(metadataCode as string) as ToolMetadata; + parsedMetadata = ToolMetadataSchema.parse(parseResult); + } catch (error) { + if (error instanceof z.ZodError) { + toast.error('Invalid Metadata JSON Value', { + description: error.issues.map((issue) => issue.message).join(', '), + }); + return; + } + + toast.error('Invalid Metadata JSON', { + position: 'top-right', + description: (error as Error)?.message, + }); + return; + } + + await saveToolCode({ + code: toolCode, + metadata: parsedMetadata, + jobId: extractJobIdFromInbox(chatInboxId), + token: auth?.api_v2_key ?? '', + nodeAddress: auth?.node_address ?? '', + language: currentLanguage, + }); + }; + + const restoreCode = async () => { + const currentIdx = toolHistory.findIndex( + (history) => history.code === toolCode, + ); + + await restoreToolConversation({ + token: auth?.api_v2_key ?? '', + nodeAddress: auth?.node_address ?? '', + jobId: extractJobIdFromInbox(chatInboxId ?? ''), + messageId: toolHistory[currentIdx].messageId, + }); + }; + + const goPreviousToolCode = () => { + const currentIdx = toolHistory.findIndex( + (history) => history.code === toolCode, + ); + const prevTool = toolHistory[currentIdx - 1]; + + const messageEl = document.getElementById(prevTool.messageId); + baseToolCodeRef.current = prevTool.code; + setToolCode(prevTool.code); + setResetCounter((prev) => prev + 1); + if (messageEl) { + // wait til requestAnimationFrame for scrolling + setTimeout(() => { + messageEl.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 100); + } + }; + + const goNextToolCode = () => { + const currentIdx = toolHistory.findIndex( + (history) => history.code === toolCode, + ); + const nextTool = toolHistory[currentIdx + 1]; + baseToolCodeRef.current = nextTool.code; + setToolCode(nextTool.code); + setResetCounter((prev) => prev + 1); + + const messageEl = document.getElementById(nextTool.messageId); + if (messageEl) { + setTimeout(() => { + messageEl.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 100); + } + }; + + const handleApplyChangesCodeSubmit = async ( + e: React.FormEvent, + ) => { + e.preventDefault(); + const data = new FormData(e.currentTarget); + const currentEditorValue = data.get('editor'); + setResetCounter((prev) => prev + 1); + setIsDirtyCodeEditor(false); + baseToolCodeRef.current = currentEditorValue as string; + // forceGenerateMetadata.current = true; // do not force it, user can regenerate it in the UI + + await updateToolCodeImplementation({ + token: auth?.api_v2_key ?? '', + nodeAddress: auth?.node_address ?? '', + jobId: extractJobIdFromInbox(chatInboxId ?? ''), + code: currentEditorValue as string, + }); + + setTimeout(() => { + setToolCode(currentEditorValue as string); + }, 100); + }; + + const resetToolCode = () => { + setIsDirtyCodeEditor(false); + setResetCounter((prev) => prev + 1); + forceGenerateMetadata.current = true; + setTimeout(() => { + setToolCode(baseToolCodeRef.current); + }, 0); + }; return { + chatInboxId, toolCode, setToolCode, baseToolCodeRef, @@ -118,12 +420,31 @@ export const useToolCode = ({ chatInboxId }: { chatInboxId?: string }) => { isToolCodeGenerationIdle, isToolCodeGenerationError, toolCodeGenerationData, + toolCodeGenerationError, fetchPreviousPage, hasPreviousPage, isChatConversationLoading, isFetchingPreviousPage, isChatConversationSuccess, - chatConversationData, + chatConversationData: conversationData, toolHistory, + codeEditorRef, + metadataEditorRef, + createToolCode, + executeToolCodeQuery, + toolResult, + forceGenerateMetadata, + isDirtyCodeEditor, + setIsDirtyCodeEditor, + resetCounter, + restoreCode, + handleCreateToolCode, + handleApplyChangesCodeSubmit, + isRestoringToolConversation, + isSavingTool, + goPreviousToolCode, + goNextToolCode, + handleSaveTool, + resetToolCode, }; }; diff --git a/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-metadata.ts b/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-metadata.ts index 3923cfa28..d85aaee84 100644 --- a/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-metadata.ts +++ b/apps/shinkai-desktop/src/components/playground-tool/hooks/use-tool-metadata.ts @@ -1,21 +1,31 @@ import { ToolMetadata } from '@shinkai_network/shinkai-message-ts/api/tools/types'; -import { useEffect, useRef, useState } from 'react'; +import { + buildInboxIdFromJobId, + extractJobIdFromInbox, +} from '@shinkai_network/shinkai-message-ts/utils/inbox_name_handler'; +import { useCreateToolMetadata } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolMetadata/useCreateToolMetadata'; +import React, { useCallback, useEffect, useState } from 'react'; import { useChatConversationWithOptimisticUpdates } from '../../../pages/chat/chat-conversation'; import { useAuth } from '../../../store/auth'; import { ToolMetadataSchema } from '../schemas'; export const useToolMetadata = ({ - chatInboxIdMetadata, + chatInboxId, + forceGenerateMetadata, initialState, + tools, + toolCode, }: { - chatInboxIdMetadata?: string; - code?: string; + chatInboxId?: string; + toolCode?: string; initialState?: { metadata: ToolMetadata | null; state?: 'idle' | 'pending' | 'success' | 'error'; error?: string | null; }; + tools: string[]; + forceGenerateMetadata: React.MutableRefObject; }) => { const [metadataData, setMetadataData] = useState( initialState?.metadata ?? null, @@ -28,7 +38,9 @@ export const useToolMetadata = ({ 'idle' | 'pending' | 'success' | 'error' >(initialState?.state ?? 'idle'); - const forceGenerateMetadata = useRef(false); + const [chatInboxIdMetadata, setChatInboxIdMetadata] = useState< + string | undefined + >(undefined); const { data: metadataChatConversation } = useChatConversationWithOptimisticUpdates({ @@ -36,12 +48,36 @@ export const useToolMetadata = ({ forceRefetchInterval: true, }); + const { mutateAsync: createToolMetadata } = useCreateToolMetadata(); + useEffect(() => { setMetadataData(initialState?.metadata ?? null); setError(initialState?.error ?? null); setMetadataState(initialState?.state ?? 'idle'); }, [initialState?.error, initialState?.metadata, initialState?.state]); + const regenerateToolMetadata = useCallback(async () => { + return await createToolMetadata( + { + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + jobId: extractJobIdFromInbox(chatInboxId ?? '') ?? '', + tools: tools, + }, + { + onSuccess: (data) => { + setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); + }, + }, + ); + }, [ + auth?.api_v2_key, + auth?.node_address, + chatInboxId, + createToolMetadata, + tools, + ]); + useEffect(() => { const metadata = metadataChatConversation?.pages?.at(-1)?.at(-1); @@ -87,6 +123,35 @@ export const useToolMetadata = ({ } }, [auth?.shinkai_identity, metadataChatConversation?.pages]); + useEffect(() => { + if (!toolCode || !forceGenerateMetadata.current) return; + const run = async () => { + await createToolMetadata( + { + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + jobId: extractJobIdFromInbox(chatInboxId ?? ''), + tools: tools, + }, + { + onSuccess: (data) => { + setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); + forceGenerateMetadata.current = false; + }, + }, + ); + }; + run(); + }, [ + auth?.api_v2_key, + auth?.node_address, + chatInboxId, + toolCode, + createToolMetadata, + forceGenerateMetadata, + tools, + ]); + return { isMetadataGenerationIdle: metadataState === 'idle', isMetadataGenerationPending: metadataState === 'pending', @@ -94,6 +159,6 @@ export const useToolMetadata = ({ isMetadataGenerationError: metadataState === 'error', metadataGenerationError: error, metadataGenerationData: metadataData, - forceGenerateMetadata, + regenerateToolMetadata, }; }; diff --git a/apps/shinkai-desktop/src/components/playground-tool/utils/code.ts b/apps/shinkai-desktop/src/components/playground-tool/utils/code.ts new file mode 100644 index 000000000..6864aaffa --- /dev/null +++ b/apps/shinkai-desktop/src/components/playground-tool/utils/code.ts @@ -0,0 +1,36 @@ +import { CodeLanguage } from '@shinkai_network/shinkai-message-ts/api/tools/types'; + +export function extractTypeScriptCode(message: string, language: CodeLanguage) { + const tsCodeMatch = message.match( + new RegExp(`\`\`\`${language.toLowerCase()}\n([\\s\\S]*?)\n\`\`\``), + ); + return tsCodeMatch ? tsCodeMatch[1].trim() : null; +} + +export function detectLanguage(code: string): string { + const pythonPatterns = [ + /\bdef\b/, + /\bclass\b/, + /\bimport\b/, + /\basync\s+def\b/, + /from\s+\w+\s+import\b/, + /#.*$/, + ]; + + const typescriptPatterns = [ + /\bfunction\b/, + /\binterface\b/, + /\btype\b\s+\w+\s*=/, + /\bexport\s+(default\s+)?/, + /\/\/.*$/, + /:\s*\w+(\[\])?/, + ]; + + const isPython = pythonPatterns.some((pattern) => pattern.test(code)); + + const isTypeScript = typescriptPatterns.some((pattern) => pattern.test(code)); + + if (isPython) return 'Python'; + if (isTypeScript) return 'TypeScript'; + return 'Unknown'; +} diff --git a/apps/shinkai-desktop/src/components/tools/python-tool.tsx b/apps/shinkai-desktop/src/components/tools/python-tool.tsx new file mode 100644 index 000000000..8da77e919 --- /dev/null +++ b/apps/shinkai-desktop/src/components/tools/python-tool.tsx @@ -0,0 +1,262 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useTranslation } from '@shinkai_network/shinkai-i18n'; +import { + PythonShinkaiTool, + ShinkaiTool, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { useExportTool } from '@shinkai_network/shinkai-node-state/v2/mutations/exportTool/useExportTool'; +import { useUpdateTool } from '@shinkai_network/shinkai-node-state/v2/mutations/updateTool/useUpdateTool'; +import { + Button, + buttonVariants, + Form, + FormField, + Switch, + TextField, +} from '@shinkai_network/shinkai-ui'; +import { formatText } from '@shinkai_network/shinkai-ui/helpers'; +import { cn } from '@shinkai_network/shinkai-ui/utils'; +import { save } from '@tauri-apps/plugin-dialog'; +import * as fs from '@tauri-apps/plugin-fs'; +import { BaseDirectory } from '@tauri-apps/plugin-fs'; +import { DownloadIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { Link, useParams } from 'react-router-dom'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +import { SubpageLayout } from '../../pages/layout/simple-layout'; +import { useAuth } from '../../store/auth'; +const jsToolSchema = z.object({ + config: z.array( + z.object({ + key_name: z.string(), + key_value: z.string().optional(), + required: z.boolean(), + }), + ), +}); +type JsToolFormSchema = z.infer; + +export default function PythonTool({ + tool, + isEnabled, + isPlaygroundTool, +}: { + tool: PythonShinkaiTool; + isEnabled: boolean; + isPlaygroundTool?: boolean; +}) { + const auth = useAuth((state) => state.auth); + const { toolKey } = useParams(); + + const { t } = useTranslation(); + const { mutateAsync: updateTool, isPending } = useUpdateTool({ + onSuccess: (_, variables) => { + if ( + 'config' in variables.toolPayload && + variables.toolPayload.config?.length > 0 + ) { + toast.success('Tool configuration updated successfully'); + } + }, + onError: (error) => { + toast.error('Failed to update tool', { + description: error.response?.data?.message ?? error.message, + }); + }, + }); + + const { mutateAsync: exportTool, isPending: isExportingTool } = useExportTool( + { + onSuccess: async (response, variables) => { + const toolName = variables.toolKey.split(':::')?.[1] ?? 'untitled_tool'; + const file = new Blob([response ?? ''], { + type: 'application/octet-stream', + }); + + const arrayBuffer = await file.arrayBuffer(); + const content = new Uint8Array(arrayBuffer); + + const savePath = await save({ + defaultPath: `${toolName}.zip`, + filters: [ + { + name: 'Zip File', + extensions: ['zip'], + }, + ], + }); + + if (!savePath) { + toast.info('File saving cancelled'); + return; + } + + await fs.writeFile(savePath, content, { + baseDir: BaseDirectory.Download, + }); + + toast.success('Tool exported successfully'); + }, + onError: (error) => { + toast.error('Failed to export tool', { + description: error.response?.data?.message ?? error.message, + }); + }, + }, + ); + + const form = useForm({ + resolver: zodResolver(jsToolSchema), + defaultValues: { + config: tool.config.map((conf) => ({ + key_name: conf.BasicConfig.key_name, + key_value: conf.BasicConfig.key_value ?? '', + required: conf.BasicConfig.required, + })), + }, + }); + + const onSubmit = async (data: JsToolFormSchema) => { + let enabled = isEnabled; + + if ( + data.config.every( + (conf) => !conf.required || (conf.required && conf.key_value !== ''), + ) + ) { + enabled = true; + } + + await updateTool({ + toolKey: toolKey ?? '', + toolType: 'Deno', + toolPayload: { + config: data.config.map((conf) => ({ + BasicConfig: { + key_name: conf.key_name, + key_value: conf.key_value, + }, + })), + } as ShinkaiTool, + isToolEnabled: enabled, + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }; + + return ( + + +
+
+

Enabled

+ { + await updateTool({ + toolKey: toolKey ?? '', + toolType: 'Deno', + toolPayload: {} as ShinkaiTool, + isToolEnabled: !isEnabled, + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + }); + }} + /> +
+ {[ + { + label: 'Description', + value: tool.description, + }, + tool.author && { + label: 'Author', + value: tool.author, + }, + tool.keywords.length > 0 && { + label: 'Keyword', + value: tool.keywords, + }, + ] + .filter((item) => !!item) + .map(({ label, value }) => ( +
+ {label} + {value} +
+ ))} + + {tool.config.length > 0 && ( +
+
Tool Configuration
+ +
+ +
+ {tool.config.map((conf, index) => ( + ( + + )} + /> + ))} +
+ +
+ +
+ )} +
+ {isPlaygroundTool && ( + + Go Playground + + )} +
+
+
+ ); +} diff --git a/apps/shinkai-desktop/src/components/tools/tool-details.tsx b/apps/shinkai-desktop/src/components/tools/tool-details.tsx index d43dc89e9..3d9d489be 100644 --- a/apps/shinkai-desktop/src/components/tools/tool-details.tsx +++ b/apps/shinkai-desktop/src/components/tools/tool-details.tsx @@ -1,6 +1,7 @@ import { DenoShinkaiTool, NetworkShinkaiTool, + PythonShinkaiTool, RustShinkaiTool, ShinkaiTool, } from '@shinkai_network/shinkai-message-ts/api/tools/types'; @@ -13,6 +14,7 @@ import { SubpageLayout } from '../../pages/layout/simple-layout'; import { useAuth } from '../../store/auth'; import DenoTool from './deno-tool'; import NetworkTool from './network-tool'; +import PythonTool from './python-tool'; import RustTool from './rust-tool'; export function isDenoShinkaiTool(tool: ShinkaiTool): tool is DenoShinkaiTool { @@ -70,6 +72,16 @@ export default function ToolDetails() { tool={tool as DenoShinkaiTool} /> ); + } else if (isSuccess && toolType === 'Python') { + return ( + playgroundTool.tool_router_key === toolKey, + )} + tool={tool as PythonShinkaiTool} + /> + ); } else if (isSuccess && toolType === 'Rust') { return ; } else if (isSuccess && toolType === 'Network') { diff --git a/apps/shinkai-desktop/src/pages/ais.tsx b/apps/shinkai-desktop/src/pages/ais.tsx index 2dfa84fd2..be68dc8d8 100644 --- a/apps/shinkai-desktop/src/pages/ais.tsx +++ b/apps/shinkai-desktop/src/pages/ais.tsx @@ -47,7 +47,6 @@ import { toast } from 'sonner'; import Agents from '../components/agent/agents'; import { useURLQueryParams } from '../hooks/use-url-query-params'; import { useAuth } from '../store/auth'; -import { useSettings } from '../store/settings'; import { useShinkaiNodeManager } from '../store/shinkai-node-manager'; import { getModelObject } from './add-ai'; import { SimpleLayout } from './layout/simple-layout'; @@ -60,7 +59,6 @@ const AIsPage = () => { nodeAddress: auth?.node_address ?? '', token: auth?.api_v2_key ?? '', }); - const optInExperimental = useSettings((state) => state.optInExperimental); const isLocalShinkaiNodeIsUse = useShinkaiNodeManager( (state) => state.isInUse, @@ -84,11 +82,7 @@ const AIsPage = () => { > { > AIs - {optInExperimental && ( - - Agents - - )} + + Agents +
@@ -153,11 +145,9 @@ const AIsPage = () => { )}
- {optInExperimental && ( - - - - )} + + + ); diff --git a/apps/shinkai-desktop/src/pages/create-tool.tsx b/apps/shinkai-desktop/src/pages/create-tool.tsx index a0c41e74b..31e3b2445 100644 --- a/apps/shinkai-desktop/src/pages/create-tool.tsx +++ b/apps/shinkai-desktop/src/pages/create-tool.tsx @@ -1,1195 +1,7 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { ReloadIcon } from '@radix-ui/react-icons'; -import { FormProps } from '@rjsf/core'; -import { RJSFSchema } from '@rjsf/utils'; -import validator from '@rjsf/validator-ajv8'; -import { useTranslation } from '@shinkai_network/shinkai-i18n'; -import { ToolMetadata } from '@shinkai_network/shinkai-message-ts/api/tools/types'; -import { - buildInboxIdFromJobId, - extractJobIdFromInbox, -} from '@shinkai_network/shinkai-message-ts/utils/inbox_name_handler'; -import { useCreateToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolCode/useCreateToolCode'; -import { useCreateToolMetadata } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolMetadata/useCreateToolMetadata'; -import { useExecuteToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/executeToolCode/useExecuteToolCode'; -import { useRestoreToolConversation } from '@shinkai_network/shinkai-node-state/v2/mutations/restoreToolConversation/useRestoreToolConversation'; -import { useSaveToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/saveToolCode/useSaveToolCode'; -import { useUpdateToolCodeImplementation } from '@shinkai_network/shinkai-node-state/v2/mutations/updateToolCodeImplementation/useUpdateToolCodeImplementation'; -import { useGetTools } from '@shinkai_network/shinkai-node-state/v2/queries/getToolsList/useGetToolsList'; -import { - Badge, - Button, - ChatInputArea, - Form, - FormControl, - FormField, - FormItem, - FormLabel, - JsonForm, - MessageList, - Popover, - PopoverContent, - PopoverTrigger, - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, - Switch, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Tooltip, - TooltipContent, - TooltipPortal, - TooltipProvider, - TooltipTrigger, -} from '@shinkai_network/shinkai-ui'; -import { SendIcon } from '@shinkai_network/shinkai-ui/assets'; -import { formatText } from '@shinkai_network/shinkai-ui/helpers'; -import { cn } from '@shinkai_network/shinkai-ui/utils'; -import { AnimatePresence, motion } from 'framer-motion'; -import { - ArrowUpRight, - BoltIcon, - Loader2, - LucideArrowLeft, - Play, - Redo2Icon, - Save, - Undo2Icon, -} from 'lucide-react'; -import { InfoCircleIcon } from 'primereact/icons/infocircle'; -import { PrismEditor } from 'prism-react-editor'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useForm, UseFormReturn } from 'react-hook-form'; -import { Link, To, useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import { AIModelSelector } from '../components/chat/chat-action-bar/ai-update-selection-action-bar'; -import { actionButtonClassnames } from '../components/chat/conversation-footer'; -import { ToolErrorFallback } from '../components/playground-tool/error-boundary'; -import { useToolCode } from '../components/playground-tool/hooks/use-tool-code'; -import { useToolMetadata } from '../components/playground-tool/hooks/use-tool-metadata'; -import PlaygroundToolLayout from '../components/playground-tool/layout'; -import { ToolMetadataSchema } from '../components/playground-tool/schemas'; -import ToolCodeEditor from '../components/playground-tool/tool-code-editor'; -import { useAuth } from '../store/auth'; -import { useSettings } from '../store/settings'; - -export const createToolCodeFormSchema = z.object({ - message: z.string().min(1), - llmProviderId: z.string().min(1), - tools: z.array(z.string()), -}); - -export type CreateToolCodeFormSchema = z.infer; - -export function extractTypeScriptCode(message: string) { - const tsCodeMatch = message.match(/```typescript\n([\s\S]*?)\n```/); - return tsCodeMatch ? tsCodeMatch[1].trim() : null; -} +import PlaygroundToolEditor from '../components/playground-tool/components/tool-playground'; function CreateToolPage() { - const auth = useAuth((state) => state.auth); - const { t } = useTranslation(); - const toolResultBoxRef = useRef(null); - const navigate = useNavigate(); - const [formData, setFormData] = useState(null); - - const [isDirty, setIsDirty] = useState(false); - const [toolResult, setToolResult] = useState(null); - const [chatInboxId, setChatInboxId] = useState(undefined); - const [chatInboxIdMetadata, setChatInboxIdMetadata] = useState< - string | undefined - >(undefined); - - const { - toolCode, - setToolCode, - baseToolCodeRef, - isToolCodeGenerationPending, - isToolCodeGenerationSuccess, - // isToolCodeGenerationIdle, - // isToolCodeGenerationError, - // toolCodeGenerationData, - fetchPreviousPage, - hasPreviousPage, - isChatConversationLoading, - isFetchingPreviousPage, - isChatConversationSuccess, - chatConversationData, - toolHistory, - } = useToolCode({ chatInboxId }); - - const { - isMetadataGenerationPending, - isMetadataGenerationSuccess, - isMetadataGenerationIdle, - metadataGenerationData, - metadataGenerationError, - isMetadataGenerationError, - forceGenerateMetadata, - } = useToolMetadata({ chatInboxIdMetadata, code: toolCode }); - - const codeEditorRef = useRef(null); - const metadataEditorRef = useRef(null); - const [resetCounter, setResetCounter] = useState(0); - - const form = useForm({ - resolver: zodResolver(createToolCodeFormSchema), - defaultValues: { - message: '', - tools: [], - }, - }); - - const { mutateAsync: createToolMetadata } = useCreateToolMetadata(); - const { - mutateAsync: executeCode, - isPending: isExecutingCode, - isSuccess: isCodeExecutionSuccess, - isError: isCodeExecutionError, - error: codeExecutionError, - reset: resetCodeExecution, - } = useExecuteToolCode({ - onSuccess: (data) => { - setToolResult(data); - toolResultBoxRef.current?.scrollIntoView({ - behavior: 'smooth', - block: 'end', - }); - }, - }); - - const { mutateAsync: updateToolCodeImplementation } = - useUpdateToolCodeImplementation(); - - const { mutateAsync: saveToolCode, isPending: isSavingTool } = - useSaveToolCode({ - onSuccess: (data) => { - toast.success('Tool code saved successfully'); - navigate(`/tools/${data.metadata.tool_router_key}`); - }, - onError: (error) => { - toast.error('Failed to save tool code', { - position: 'top-right', - description: error.response?.data?.message ?? error.message, - }); - }, - }); - - const { - mutateAsync: restoreToolConversation, - isPending: isRestoringToolConversation, - } = useRestoreToolConversation({ - onSuccess: () => { - toast.success('Successfully restore changes'); - }, - onError: (error) => { - toast.error('Failed to restore tool conversation', { - position: 'top-right', - description: error.response?.data?.message ?? error.message, - }); - }, - }); - - const defaultAgentId = useSettings( - (settingsStore) => settingsStore.defaultAgentId, - ); - - const { mutateAsync: createToolCode } = useCreateToolCode({ - onSuccess: (data) => { - setChatInboxId(data.inbox); - setToolCode(''); - forceGenerateMetadata.current = true; - baseToolCodeRef.current = ''; - setToolResult(null); - resetCodeExecution(); - }, - }); - - const regenerateToolMetadata = useCallback(async () => { - return await createToolMetadata( - { - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - jobId: extractJobIdFromInbox(chatInboxId ?? '') ?? '', - tools: form.getValues('tools'), - }, - { - onSuccess: (data) => { - setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); - }, - }, - ); - }, [auth?.api_v2_key, auth?.node_address, chatInboxId, createToolMetadata]); - - useEffect(() => { - form.setValue('llmProviderId', defaultAgentId); - }, [form, defaultAgentId]); - - useEffect(() => { - if (!toolCode || !forceGenerateMetadata.current) return; - const run = async () => { - await createToolMetadata( - { - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - jobId: extractJobIdFromInbox(chatInboxId ?? ''), - tools: form.getValues('tools'), - }, - { - onSuccess: (data) => { - setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); - forceGenerateMetadata.current = false; - }, - }, - ); - }; - run(); - }, [ - auth?.api_v2_key, - auth?.node_address, - createToolCode, - defaultAgentId, - toolCode, - ]); - - const onSubmit = async (data: CreateToolCodeFormSchema) => { - if (!auth) return; - - await createToolCode({ - nodeAddress: auth.node_address, - token: auth.api_v2_key, - message: data.message, - llmProviderId: data.llmProviderId, - jobId: chatInboxId ? extractJobIdFromInbox(chatInboxId ?? '') : undefined, - tools: data.tools, - }); - - form.setValue('message', ''); - return; - }; - - const handleRunCode: FormProps['onSubmit'] = async (data) => { - const params = data.formData; - const updatedCodeWithoutSave = codeEditorRef.current?.value ?? ''; - await executeCode({ - code: isDirty ? updatedCodeWithoutSave : toolCode, - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - params, - llmProviderId: form.getValues('llmProviderId'), - tools: form.getValues('tools'), - }); - }; - - const handleSaveTool = async () => { - if (!chatInboxId) return; - - const metadataCode = metadataEditorRef.current?.value; - const toolCode = codeEditorRef.current?.value; - - let parsedMetadata: ToolMetadata; - - try { - const parseResult = JSON.parse(metadataCode as string) as ToolMetadata; - parsedMetadata = ToolMetadataSchema.parse(parseResult); - } catch (error) { - if (error instanceof z.ZodError) { - toast.error('Invalid Metadata JSON Value', { - description: error.issues.map((issue) => issue.message).join(', '), - }); - return; - } - - toast.error('Invalid Metadata JSON', { - position: 'top-right', - description: (error as Error)?.message, - }); - return; - } - - await saveToolCode({ - code: toolCode, - metadata: parsedMetadata, - jobId: extractJobIdFromInbox(chatInboxId), - token: auth?.api_v2_key ?? '', - nodeAddress: auth?.node_address ?? '', - }); - }; - - return ( - -
- - - {t('common.back')} - -

- Tool Playground -

-
-
- {!chatInboxId && ( - <> - - 🤖 - -

- Generate your tool using AI -

-

- Ask Shinkai AI to generate a tool for you. Provide a prompt - and Shinkai AI will generate a tool code for you. -

-
- {[ - { - text: 'Tool for downloading a website content in markdown', - prompt: - 'Generate a tool for downloading a website into markdown', - }, - { - text: 'Tool for getting tech-related stories from Hacker News', - prompt: - 'Generate a tool for getting top tech-related stories from Hacker News, include the title, author, and URL of the story', - }, - ].map((suggestion) => ( - - form.setValue('message', suggestion.prompt) - } - variant="outline" - > - {suggestion.text} - - - ))} -
- - )} - - {chatInboxId && ( - - )} -
- -
- -
- ( - - - {t('chat.enterMessage')} - - -
-
- { - form.setValue('llmProviderId', value); - }} - value={form.watch('llmProviderId')} - /> - -
- - - Enter to - send - - -
- } - disabled={ - isToolCodeGenerationPending || - isMetadataGenerationPending - } - onChange={field.onChange} - onSubmit={form.handleSubmit(onSubmit)} - topAddons={<>} - value={field.value} - /> -
- - - )} - /> - -
- - - } - rightElement={ - -
-
-
- - - Code - - - Metadata - - -
- {toolHistory.length > 1 && ( -
- {toolCode === toolHistory?.at(-1)?.code ? ( - - - - Latest - - - - -

This is your latest version

-
-
-
- ) : ( - - - { - const currentIdx = toolHistory.findIndex( - (history) => history.code === toolCode, - ); - - await restoreToolConversation({ - token: auth?.api_v2_key ?? '', - nodeAddress: auth?.node_address ?? '', - jobId: extractJobIdFromInbox( - chatInboxId ?? '', - ), - messageId: toolHistory[currentIdx].messageId, - }); - }} - variant="secondary" - > - {isRestoringToolConversation ? ( - - ) : null} - Restore - - - - -

- Restore to this version. This action will undo - all changes made since the selected version -

-
-
-
- )} -
- - - - - - -

View previous version

-
-
-
- - - - - - -

View next version

-
-
-
-
-
- )} - -
-
- - - -
-
-
-
-

- Code{' '} -

- {toolCode && ( -

- {/* eslint-disable-next-line react/no-unescaped-entities */} - Here's the code generated by Shinkai AI based on - your prompt. -

- )} -
- - {/*{toolCode && (*/} - {/* */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - {/* */} - {/* */} - {/*

Copy Code

*/} - {/*
*/} - {/*
*/} - {/*
*/} - {/*)}*/} -
-
- {isToolCodeGenerationPending && ( -
- - Generating Code... -
- )} - {!isToolCodeGenerationPending && - !toolCode && - !isToolCodeGenerationSuccess && ( -

- No code generated yet.
- Ask Shinkai AI to generate your tool code. -

- )} - {isToolCodeGenerationSuccess && toolCode && ( -
{ - e.preventDefault(); - const data = new FormData(e.currentTarget); - const currentEditorValue = data.get('editor'); - setResetCounter((prev) => prev + 1); - setIsDirty(false); - baseToolCodeRef.current = - currentEditorValue as string; - // forceGenerateMetadata.current = true; // do not force it, user can regenerate it in the UI - - await updateToolCodeImplementation({ - token: auth?.api_v2_key ?? '', - nodeAddress: auth?.node_address ?? '', - jobId: extractJobIdFromInbox( - chatInboxId ?? '', - ), - code: currentEditorValue as string, - }); - - setTimeout(() => { - setToolCode(currentEditorValue as string); - }, 0); - }} - > -
- - {' '} - TypeScript - {isDirty && ( - - )} - - - {isDirty && ( - - - - - )} - -
- { - setIsDirty( - currentCode !== baseToolCodeRef.current, - ); - }} - ref={codeEditorRef} - value={toolCode} - /> - - )} -
-
-
-
- - - -
-
-
-

- Run -

- {metadataGenerationData && ( -

Fill in the options above to run your tool.

- )} -
- {isMetadataGenerationSuccess && - !isToolCodeGenerationPending && - !isMetadataGenerationError && ( - - )} -
-
- {(isMetadataGenerationPending || - isToolCodeGenerationPending) && ( -
- - Generating... -
- )} - {!isMetadataGenerationPending && - !isToolCodeGenerationPending && - isMetadataGenerationError && ( - - )} - {isMetadataGenerationSuccess && - !isToolCodeGenerationPending && - !isMetadataGenerationError && ( -
- setFormData(e.formData)} - onSubmit={handleRunCode} - schema={ - metadataGenerationData?.parameters as RJSFSchema - } - uiSchema={{ - 'ui:submitButtonOptions': { - norender: true, - }, - }} - validator={validator} - /> - - {(isExecutingCode || - isCodeExecutionError || - isCodeExecutionSuccess) && ( - - {isExecutingCode && ( -
- - Running Tool... -
- )} - {isCodeExecutionError && ( -
-

- Tool execution failed. Try generating - the tool code again. -

-
-                                          {codeExecutionError?.response?.data
-                                            ?.message ??
-                                            codeExecutionError?.message}
-                                        
-
- )} -
- {isCodeExecutionSuccess && toolResult && ( - - )} -
-
- )} -
-
- )} - {isMetadataGenerationIdle && - !isToolCodeGenerationPending && ( -
-

- No metadata generated yet. -

-
- )} -
-
-
-
-
- -
-
-
-

- Metadata -

- {metadataGenerationData && ( -

Fill in the options above to run your tool.

- )} -
- {isMetadataGenerationSuccess && ( - - )} -
- {isMetadataGenerationPending && ( -
- - Generating Metadata... -
- )} - {!isMetadataGenerationPending && - !isToolCodeGenerationPending && - isMetadataGenerationError && ( - - )} - - {isMetadataGenerationSuccess && - !isMetadataGenerationError && ( -
-
- -
-
- )} - {isMetadataGenerationIdle && ( -
-

- No metadata generated yet. -

-
- )} -
-
-
-
-
- } - /> - ); + return ; } export default CreateToolPage; - -export function ToolSelectionModal({ - form, -}: { - form: UseFormReturn; -}) { - const auth = useAuth((state) => state.auth); - - const { data: toolsList } = useGetTools({ - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - }); - - return ( - - - -
- Tools{' '} - - {form.watch('tools').length} - -
-
- -
-

Available tools

-
- { - const isAllConfigFilled = toolsList - ?.map((tool) => tool.config) - .filter((item) => !!item) - .flat() - ?.map((conf) => ({ - key_name: conf.BasicConfig.key_name, - key_value: conf.BasicConfig.key_value ?? '', - required: conf.BasicConfig.required, - })) - .every( - (conf) => - !conf.required || - (conf.required && conf.key_value !== ''), - ); - if (!isAllConfigFilled) { - toast.error('Tool configuration', { - description: - 'Please fill in the config required in tool details', - }); - return; - } - - if (checked && toolsList) { - form.setValue( - 'tools', - toolsList.map((tool) => tool.tool_router_key), - ); - } else { - form.setValue('tools', []); - } - }} - /> - -
-
- -
- {toolsList?.map((tool) => ( - ( - - -
- { - const configs = tool?.config ?? []; - if ( - configs - .map((conf) => ({ - key_name: conf.BasicConfig.key_name, - key_value: conf.BasicConfig.key_value ?? '', - required: conf.BasicConfig.required, - })) - .every( - (conf) => - !conf.required || - (conf.required && conf.key_value !== ''), - ) - ) { - field.onChange( - field.value.includes(tool.tool_router_key) - ? field.value.filter( - (value) => value !== tool.tool_router_key, - ) - : [...field.value, tool.tool_router_key], - ); - - return; - } - toast.error('Tool configuration is required', { - description: - 'Please fill in the config required in tool details', - }); - }} - /> -
- - - - - - - - {tool.description} - - - -
- {(tool.config ?? []).length > 0 && ( - - - - - - - - -

Configure tool

-
-
-
- )} -
-
-
- )} - /> - ))} -
-
-
-
- ); -} diff --git a/apps/shinkai-desktop/src/pages/edit-tool.tsx b/apps/shinkai-desktop/src/pages/edit-tool.tsx index 593b175a6..a889896ba 100644 --- a/apps/shinkai-desktop/src/pages/edit-tool.tsx +++ b/apps/shinkai-desktop/src/pages/edit-tool.tsx @@ -1,73 +1,13 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { ReloadIcon } from '@radix-ui/react-icons'; -import { FormProps } from '@rjsf/core'; -import { RJSFSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; -import { useTranslation } from '@shinkai_network/shinkai-i18n'; -import { ToolMetadata } from '@shinkai_network/shinkai-message-ts/api/tools/types'; -import { - buildInboxIdFromJobId, - extractJobIdFromInbox, -} from '@shinkai_network/shinkai-message-ts/utils/inbox_name_handler'; -import { useCreateToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolCode/useCreateToolCode'; -import { useCreateToolMetadata } from '@shinkai_network/shinkai-node-state/v2/mutations/createToolMetadata/useCreateToolMetadata'; -import { useExecuteToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/executeToolCode/useExecuteToolCode'; -import { useSaveToolCode } from '@shinkai_network/shinkai-node-state/v2/mutations/saveToolCode/useSaveToolCode'; +import { CodeLanguage } from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { buildInboxIdFromJobId } from '@shinkai_network/shinkai-message-ts/utils/inbox_name_handler'; import { useGetPlaygroundTool } from '@shinkai_network/shinkai-node-state/v2/queries/getPlaygroundTool/useGetPlaygroundTool'; -import { - Badge, - Button, - ChatInputArea, - Form, - FormControl, - FormField, - FormItem, - FormLabel, - JsonForm, - MessageList, - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from '@shinkai_network/shinkai-ui'; -import { SendIcon } from '@shinkai_network/shinkai-ui/assets'; -import { cn } from '@shinkai_network/shinkai-ui/utils'; -import { AnimatePresence, motion } from 'framer-motion'; -import { - ArrowUpRight, - Loader2, - LucideArrowLeft, - Play, - Save, -} from 'lucide-react'; -import { PrismEditor } from 'prism-react-editor'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { Link, To, useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'sonner'; -import { z } from 'zod'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; -import { AIModelSelector } from '../components/chat/chat-action-bar/ai-update-selection-action-bar'; -import { ToolErrorFallback } from '../components/playground-tool/error-boundary'; -import { useToolCode } from '../components/playground-tool/hooks/use-tool-code'; -import { useToolMetadata } from '../components/playground-tool/hooks/use-tool-metadata'; -import PlaygroundToolLayout from '../components/playground-tool/layout'; -import { ToolMetadataSchema } from '../components/playground-tool/schemas'; -import ToolCodeEditor from '../components/playground-tool/tool-code-editor'; +import PlaygroundToolEditor from '../components/playground-tool/components/tool-playground'; +import { detectLanguage } from '../components/playground-tool/utils/code'; import { useAuth } from '../store/auth'; -import { useSettings } from '../store/settings'; -import { ToolSelectionModal } from './create-tool'; - -export const createToolCodeFormSchema = z.object({ - message: z.string().min(1), - llmProviderId: z.string().min(1), - tools: z.array(z.string()), -}); - -export type CreateToolCodeFormSchema = z.infer; function EditToolPage() { const auth = useAuth((state) => state.auth); @@ -84,48 +24,33 @@ function EditToolPage() { toolRouterKey: toolRouterKey ?? '', }); - const { t } = useTranslation(); - const toolResultBoxRef = useRef(null); - const navigate = useNavigate(); - const [formData, setFormData] = useState(null); - - const [isDirty, setIsDirty] = useState(false); - const [toolResult, setToolResult] = useState(null); - const chatInboxId = playgroundTool ? buildInboxIdFromJobId(playgroundTool.job_id) : ''; - const [chatInboxIdMetadata, setChatInboxIdMetadata] = useState< - string | undefined - >(undefined); - const codeEditorRef = useRef(null); - const metadataEditorRef = useRef(null); - - const [resetCounter, setResetCounter] = useState(0); - - const { - toolCode, - setToolCode, - baseToolCodeRef, - isToolCodeGenerationPending, - isToolCodeGenerationSuccess, - // isToolCodeGenerationIdle, - // isToolCodeGenerationError, - // toolCodeGenerationData, - fetchPreviousPage, - hasPreviousPage, - isChatConversationLoading, - isFetchingPreviousPage, - isChatConversationSuccess, - chatConversationData, - } = useToolCode({ chatInboxId }); + const toolCodeInitialValues = useMemo( + () => ({ + code: playgroundTool?.code ?? '', + error: null, + state: isPlaygroundToolPending + ? 'pending' + : isPlaygroundToolSuccess + ? 'success' + : ('idle' as 'idle' | 'pending' | 'success' | 'error'), + }), + [ + isPlaygroundToolError, + isPlaygroundToolPending, + isPlaygroundToolSuccess, + playgroundTool?.code, + ], + ); const isValidSchema = validator.ajv.validateSchema( playgroundTool?.metadata?.parameters ?? {}, ); - const initialState = useMemo( + const toolMetadataInitialValues = useMemo( () => ({ metadata: playgroundTool?.metadata ?? null, state: isPlaygroundToolPending @@ -146,717 +71,16 @@ function EditToolPage() { ], ); - const { - isMetadataGenerationPending, - isMetadataGenerationSuccess, - isMetadataGenerationIdle, - metadataGenerationData, - metadataGenerationError, - isMetadataGenerationError, - forceGenerateMetadata, - } = useToolMetadata({ - chatInboxIdMetadata, - code: toolCode, - initialState, - }); - - const form = useForm({ - resolver: zodResolver(createToolCodeFormSchema), - defaultValues: { - message: '', - tools: [], - }, - }); - - const { mutateAsync: createToolMetadata } = useCreateToolMetadata(); - const { - mutateAsync: executeCode, - isPending: isExecutingCode, - isSuccess: isCodeExecutionSuccess, - isError: isCodeExecutionError, - error: codeExecutionError, - } = useExecuteToolCode({ - onSuccess: (data) => { - setToolResult(data); - toolResultBoxRef.current?.scrollIntoView({ - behavior: 'smooth', - block: 'end', - }); - }, - }); - - const { mutateAsync: saveToolCode, isPending: isSavingTool } = - useSaveToolCode({ - onSuccess: (data) => { - toast.success('Tool code saved successfully'); - navigate(`/tools/${data.metadata.tool_router_key}`); - }, - onError: (error) => { - toast.error('Failed to save tool code', { - position: 'top-right', - description: error.response?.data?.message ?? error.message, - }); - }, - }); - - const defaultAgentId = useSettings( - (settingsStore) => settingsStore.defaultAgentId, - ); - const { mutateAsync: createToolCode } = useCreateToolCode(); - - const regenerateToolMetadata = useCallback(async () => { - return await createToolMetadata( - { - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - jobId: extractJobIdFromInbox(chatInboxId ?? '') ?? '', - tools: form.getValues('tools'), - }, - { - onSuccess: (data) => { - setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); - }, - }, - ); - }, [ - auth?.api_v2_key, - auth?.node_address, - chatInboxId, - createToolMetadata, - form, - ]); - - useEffect(() => { - form.setValue('llmProviderId', defaultAgentId); - }, [form, defaultAgentId]); - - useEffect(() => { - if (!toolCode || !forceGenerateMetadata.current) return; - const run = async () => { - await createToolMetadata( - { - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - jobId: extractJobIdFromInbox(chatInboxId ?? ''), - tools: form.getValues('tools'), - }, - { - onSuccess: (data) => { - setChatInboxIdMetadata(buildInboxIdFromJobId(data.job_id)); - forceGenerateMetadata.current = false; - }, - }, - ); - }; - run(); - }, [ - auth?.api_v2_key, - auth?.node_address, - createToolCode, - defaultAgentId, - toolCode, - ]); - - const onSubmit = async (data: CreateToolCodeFormSchema) => { - if (!auth || !chatInboxId) return; - await createToolCode( - { - nodeAddress: auth.node_address, - token: auth.api_v2_key, - message: data.message, - llmProviderId: data.llmProviderId, - tools: data.tools, - jobId: playgroundTool?.job_id, - }, - { - onSuccess: () => { - setToolCode(''); - forceGenerateMetadata.current = true; - baseToolCodeRef.current = ''; - setToolResult(null); - }, - }, - ); - - form.setValue('message', ''); - return; - }; - - const handleRunCode: FormProps['onSubmit'] = async (data) => { - const params = data.formData; - await executeCode({ - code: toolCode, - nodeAddress: auth?.node_address ?? '', - token: auth?.api_v2_key ?? '', - params, - llmProviderId: form.getValues('llmProviderId'), - tools: form.getValues('tools'), - }); - }; - - const handleSaveTool = async () => { - if (!chatInboxId) return; - - const metadataCode = metadataEditorRef.current?.value; - const toolCode = codeEditorRef.current?.value; - - let parsedMetadata: ToolMetadata; - try { - const parseResult = JSON.parse(metadataCode as string) as ToolMetadata; - parsedMetadata = ToolMetadataSchema.parse(parseResult); - } catch (error) { - if (error instanceof z.ZodError) { - toast.error('Invalid Metadata JSON Value', { - description: error.issues.map((issue) => issue.message).join(', '), - }); - return; - } - - toast.error('Invalid Metadata JSON', { - position: 'top-right', - description: (error as Error)?.message, - }); - return; - } - - await saveToolCode({ - code: toolCode, - metadata: parsedMetadata, - jobId: extractJobIdFromInbox(chatInboxId), - token: auth?.api_v2_key ?? '', - nodeAddress: auth?.node_address ?? '', - }); - }; - return ( - -
- - - {t('common.back')} - -

- {playgroundTool?.metadata.name} -

-
-
- {!chatInboxId && ( - <> - - 🤖 - -

- Generate your tool using AI -

-

- Ask Shinkai AI to generate a tool for you. Provide a prompt - and Shinkai AI will generate a tool code for you. -

-
- {[ - { - text: 'Generate a tool that downloads https://jhftss.github.io/', - prompt: - 'Generate a tool that downloads https://jhftss.github.io/', - }, - ].map((suggestion) => ( - - form.setValue('message', suggestion.prompt) - } - variant="outline" - > - {suggestion.text} - - - ))} -
- - )} - {chatInboxId && ( - - )} -
- -
- -
- ( - - - {t('chat.enterMessage')} - - -
-
- { - form.setValue('llmProviderId', value); - }} - value={form.watch('llmProviderId')} - /> - -
- - - Enter to - send - - -
- } - disabled={ - isToolCodeGenerationPending || - isMetadataGenerationPending - } - onChange={field.onChange} - onSubmit={form.handleSubmit(onSubmit)} - topAddons={<>} - value={field.value} - /> -
- - - )} - /> - -
- - - } - rightElement={ - -
-
-
- - - Code - - - Metadata - - -
- -
-
- - - - -
-
-
-

- Code{' '} -

- {toolCode && ( -

- {/* eslint-disable-next-line react/no-unescaped-entities */} - Here's the code generated by Shinkai AI based on - your prompt. -

- )} -
- {/*{toolCode && (*/} - {/* */} - {/* */} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - {/* */} - {/* */} - {/*

Copy Code

*/} - {/*
*/} - {/*
*/} - {/*
*/} - {/*)}*/} -
- {isToolCodeGenerationPending && ( -
- - Generating Code... -
- )} - {!isToolCodeGenerationPending && - !toolCode && - !isToolCodeGenerationSuccess && ( -

- No code generated yet.
- Ask Shinkai AI to generate your tool code. -

- )} - {isToolCodeGenerationSuccess && toolCode && ( -
{ - e.preventDefault(); - const data = new FormData(e.currentTarget); - const currentEditorValue = data.get('editor'); - setResetCounter((prev) => prev + 1); - setIsDirty(false); - baseToolCodeRef.current = - currentEditorValue as string; - // forceGenerateMetadata.current = true; - setTimeout(() => { - setToolCode(currentEditorValue as string); - }, 0); - }} - > -
- - {' '} - TypeScript - {isDirty && ( - - )} - - - {isDirty && ( - - - - - )} - -
- { - setIsDirty( - currentCode !== baseToolCodeRef.current, - ); - }} - value={toolCode} - /> - - )} -
-
-
-
- - - -
-
-
-
-

- Run -

- {metadataGenerationData && ( -

Fill in the options above to run your tool.

- )} -
- {isMetadataGenerationSuccess && - !isToolCodeGenerationPending && - !isMetadataGenerationError && ( - - )} -
-
- {(isMetadataGenerationPending || - isToolCodeGenerationPending) && ( -
- - Generating... -
- )} - {!isToolCodeGenerationPending && - isMetadataGenerationError && ( - - )} - {!isToolCodeGenerationPending && - isMetadataGenerationSuccess && ( -
- {!isMetadataGenerationPending && ( - setFormData(e.formData)} - onSubmit={handleRunCode} - schema={ - metadataGenerationData?.parameters as RJSFSchema - } - uiSchema={{ - 'ui:submitButtonOptions': { - norender: true, - }, - }} - validator={validator} - /> - )} - - {(isExecutingCode || - isCodeExecutionError || - isCodeExecutionSuccess) && ( - - {isExecutingCode && ( -
- - Running Tool... -
- )} - {isCodeExecutionError && ( -
-

- Tool execution failed. Try - generating the tool code again. -

-
-                                            {codeExecutionError?.response?.data
-                                              ?.message ??
-                                              codeExecutionError?.message}
-                                          
-
- )} - {isCodeExecutionSuccess && toolResult && ( -
- -
- )} -
- )} -
-
- )} - {isMetadataGenerationIdle && - !isToolCodeGenerationPending && ( -
-

- No metadata generated yet. -

-
- )} -
-
-
-
-
-
- -
-
-
-

- Metadata -

- {metadataGenerationData && ( -

Fill in the options above to run your tool.

- )} -
- {isMetadataGenerationSuccess && ( - - )} -
- {isMetadataGenerationPending && ( -
- - Generating Metadata... -
- )} - {!isMetadataGenerationPending && - !isToolCodeGenerationPending && - isMetadataGenerationError && ( - - )} - {isMetadataGenerationSuccess && - !isMetadataGenerationError && ( -
-
- -
-
- )} - - {isMetadataGenerationIdle && ( -
-

- No metadata generated yet. -

-
- )} -
-
-
-
-
- } + ); } diff --git a/apps/shinkai-desktop/src/pages/tools.tsx b/apps/shinkai-desktop/src/pages/tools.tsx index 0a2c48ab1..5f2f577de 100644 --- a/apps/shinkai-desktop/src/pages/tools.tsx +++ b/apps/shinkai-desktop/src/pages/tools.tsx @@ -64,7 +64,7 @@ export const Tools = () => { token: auth?.api_v2_key ?? '', search: debouncedSearchQuery, }, - { enabled: isSearchQuerySynced }, + { enabled: isSearchQuerySynced && !!searchQuery }, ); const { mutateAsync: updateTool } = useUpdateTool(); diff --git a/libs/shinkai-message-ts/src/api/tools/index.ts b/libs/shinkai-message-ts/src/api/tools/index.ts index b48a0fab3..c14afcca9 100644 --- a/libs/shinkai-message-ts/src/api/tools/index.ts +++ b/libs/shinkai-message-ts/src/api/tools/index.ts @@ -19,7 +19,6 @@ import { GetPlaygroundToolsResponse, GetToolResponse, GetToolsResponse, - GetToolsSearchResponse, ImportToolRequest, ImportToolResponse, PayInvoiceRequest, @@ -91,9 +90,7 @@ export const searchTools = async ( responseType: 'json', }, ); - const data = response.data as GetToolsSearchResponse; - const formattedData = data.map(([header]) => header); - return formattedData as GetToolsResponse; + return response.data as GetToolsResponse; }; export const updateTool = async ( diff --git a/libs/shinkai-message-ts/src/api/tools/types.ts b/libs/shinkai-message-ts/src/api/tools/types.ts index 09d920d8d..0b10cadfe 100644 --- a/libs/shinkai-message-ts/src/api/tools/types.ts +++ b/libs/shinkai-message-ts/src/api/tools/types.ts @@ -51,6 +51,21 @@ export type DenoShinkaiTool = { config_set: boolean; activated: boolean; embedding?: Embedding; + result: Record; +}; +export type PythonShinkaiTool = { + activated: boolean; + author: string; + keywords: string[]; + input_args: ToolArgument[]; + description: string; + config: ToolConfig[]; + name: string; + oauth: string; + py_code: string; + output_arg: { + json: string; + }; result: JSToolResult; }; export type RustShinkaiTool = { @@ -81,9 +96,10 @@ export type NetworkShinkaiTool = { version: string; }; -export type ShinkaiToolType = 'Deno' | 'Rust' | 'Network'; +export type ShinkaiToolType = 'Deno' | 'Python' | 'Rust' | 'Network'; export type ShinkaiTool = | DenoShinkaiTool + | PythonShinkaiTool | RustShinkaiTool | NetworkShinkaiTool; @@ -199,7 +215,7 @@ export type ExecuteToolCodeRequest = { tool_type: DynamicToolType; parameters: Record; code: string; - extra_config?: string; + extra_config?: Record; llm_provider: string; tools: string[]; }; @@ -212,6 +228,7 @@ export type SaveToolCodeRequest = { job_id: string; job_id_history?: string[]; code: string; + language: CodeLanguage; }; export type ToolMetadata = { diff --git a/libs/shinkai-node-state/src/v2/mutations/createToolCode/index.ts b/libs/shinkai-node-state/src/v2/mutations/createToolCode/index.ts index 068bc1718..5140a5f5e 100644 --- a/libs/shinkai-node-state/src/v2/mutations/createToolCode/index.ts +++ b/libs/shinkai-node-state/src/v2/mutations/createToolCode/index.ts @@ -15,6 +15,7 @@ export const createToolCode = async ({ message, jobId, tools, + language, }: CreateToolCodeInput) => { let currentJobId = jobId; if (!currentJobId) { @@ -56,6 +57,6 @@ export const createToolCode = async ({ files_inbox: '', }, tools, - language: CodeLanguage.Typescript, + language, }); }; diff --git a/libs/shinkai-node-state/src/v2/mutations/createToolCode/types.ts b/libs/shinkai-node-state/src/v2/mutations/createToolCode/types.ts index f576b8fcd..4efd95f95 100644 --- a/libs/shinkai-node-state/src/v2/mutations/createToolCode/types.ts +++ b/libs/shinkai-node-state/src/v2/mutations/createToolCode/types.ts @@ -1,5 +1,8 @@ import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; -import { CreateToolCodeResponse } from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { + CodeLanguage, + CreateToolCodeResponse, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; export type CreateToolCodeInput = Token & { nodeAddress: string; @@ -7,6 +10,7 @@ export type CreateToolCodeInput = Token & { llmProviderId: string; jobId?: string; tools: string[]; + language: CodeLanguage; }; export type CreateToolCodeOutput = CreateToolCodeResponse; diff --git a/libs/shinkai-node-state/src/v2/mutations/executeToolCode/index.ts b/libs/shinkai-node-state/src/v2/mutations/executeToolCode/index.ts index 8172a1548..f402e2a24 100644 --- a/libs/shinkai-node-state/src/v2/mutations/executeToolCode/index.ts +++ b/libs/shinkai-node-state/src/v2/mutations/executeToolCode/index.ts @@ -1,5 +1,8 @@ import { executeToolCode as executeToolCodeApi } from '@shinkai_network/shinkai-message-ts/api/tools/index'; -import { DynamicToolType } from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { + CodeLanguage, + DynamicToolType, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; import { ExecuteToolCodeInput } from './types'; @@ -10,12 +13,20 @@ export const executeToolCode = async ({ params, llmProviderId, tools, + language, + configs, }: ExecuteToolCodeInput) => { + const toolTypeLanguageMap = { + [CodeLanguage.Python]: DynamicToolType.PythonDynamic, + [CodeLanguage.Typescript]: DynamicToolType.DenoDynamic, + }; + return await executeToolCodeApi(nodeAddress, token, { - tool_type: DynamicToolType.DenoDynamic, + tool_type: toolTypeLanguageMap[language], code, - parameters: params, + parameters: params ?? {}, llm_provider: llmProviderId, tools, + extra_config: configs, }); }; diff --git a/libs/shinkai-node-state/src/v2/mutations/executeToolCode/types.ts b/libs/shinkai-node-state/src/v2/mutations/executeToolCode/types.ts index c4d9068ce..f30015934 100644 --- a/libs/shinkai-node-state/src/v2/mutations/executeToolCode/types.ts +++ b/libs/shinkai-node-state/src/v2/mutations/executeToolCode/types.ts @@ -1,12 +1,17 @@ import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; -import { ExecuteToolCodeResponse } from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { + CodeLanguage, + ExecuteToolCodeResponse, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; export type ExecuteToolCodeInput = Token & { nodeAddress: string; params: Record; + configs?: Record; code: string; llmProviderId: string; tools: string[]; + language: CodeLanguage; }; export type ExecuteToolCodeOutput = ExecuteToolCodeResponse; diff --git a/libs/shinkai-node-state/src/v2/mutations/saveToolCode/index.ts b/libs/shinkai-node-state/src/v2/mutations/saveToolCode/index.ts index 74bfb987b..80ca7498c 100644 --- a/libs/shinkai-node-state/src/v2/mutations/saveToolCode/index.ts +++ b/libs/shinkai-node-state/src/v2/mutations/saveToolCode/index.ts @@ -8,10 +8,12 @@ export const saveToolCode = async ({ jobId, metadata, code, + language, }: SaveToolCodeInput) => { return await saveToolCodeApi(nodeAddress, token, { code: code ?? '', metadata, job_id: jobId, + language, }); }; diff --git a/libs/shinkai-node-state/src/v2/mutations/saveToolCode/types.ts b/libs/shinkai-node-state/src/v2/mutations/saveToolCode/types.ts index 000c7d592..d5d040259 100644 --- a/libs/shinkai-node-state/src/v2/mutations/saveToolCode/types.ts +++ b/libs/shinkai-node-state/src/v2/mutations/saveToolCode/types.ts @@ -1,11 +1,15 @@ import { Token } from '@shinkai_network/shinkai-message-ts/api/general/types'; -import { SaveToolCodeResponse } from '@shinkai_network/shinkai-message-ts/api/tools/types'; +import { + CodeLanguage, + SaveToolCodeResponse, +} from '@shinkai_network/shinkai-message-ts/api/tools/types'; export type SaveToolCodeInput = Token & { nodeAddress: string; jobId: string; metadata: Record; code?: string; + language: CodeLanguage; }; export type SaveToolCodeOutput = SaveToolCodeResponse; diff --git a/libs/shinkai-ui/src/components/chat/message-list.tsx b/libs/shinkai-ui/src/components/chat/message-list.tsx index 2fe0ddb50..6c4570d38 100644 --- a/libs/shinkai-ui/src/components/chat/message-list.tsx +++ b/libs/shinkai-ui/src/components/chat/message-list.tsx @@ -69,6 +69,7 @@ export const MessageList = ({ setArtifact, artifacts, artifact, + hidePythonExecution, }: { noMoreMessageLabel: string; isSuccess: boolean; @@ -91,6 +92,7 @@ export const MessageList = ({ artifacts?: Artifact[]; setArtifact?: (artifact: Artifact | null) => void; artifact?: Artifact; + hidePythonExecution?: boolean; }) => { const chatContainerRef = useRef(null); const previousChatHeightRef = useRef(0); @@ -311,6 +313,7 @@ export const MessageList = ({ ? handleFirstMessageRetry : handleRetryMessage } + hidePythonExecution={hidePythonExecution} key={`${message.messageId}::${messageIndex}`} message={message} messageId={message.messageId} diff --git a/libs/shinkai-ui/src/components/chat/message.tsx b/libs/shinkai-ui/src/components/chat/message.tsx index 25c12cd82..04896245c 100644 --- a/libs/shinkai-ui/src/components/chat/message.tsx +++ b/libs/shinkai-ui/src/components/chat/message.tsx @@ -8,7 +8,7 @@ import { Artifact } from '@shinkai_network/shinkai-node-state/v2/queries/getChat import { FormattedMessage } from '@shinkai_network/shinkai-node-state/v2/queries/getChatConversation/types'; import { format } from 'date-fns'; import { AnimatePresence, motion } from 'framer-motion'; -import { Code, Edit3, Loader2, RotateCcw, XCircle } from 'lucide-react'; +import { Edit3, Loader2, RotateCcw, XCircle } from 'lucide-react'; import { InfoCircleIcon } from 'primereact/icons/infocircle'; import React, { Fragment, memo, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -30,7 +30,7 @@ import { Card, CardContent } from '../card'; import { CopyToClipboardIcon } from '../copy-to-clipboard-icon'; import { DotsLoader } from '../dots-loader'; import { Form, FormField } from '../form'; -import { MarkdownText, MarkdownTextPrimitive } from '../markdown-preview'; +import { MarkdownText } from '../markdown-preview'; import { Tooltip, TooltipContent, @@ -75,6 +75,7 @@ type MessageProps = { setArtifactPanel?: (open: boolean) => void; artifacts?: Artifact[]; artifact?: Artifact; + hidePythonExecution?: boolean; }; const actionBar = { @@ -162,6 +163,7 @@ const MessageBase = ({ setArtifact, artifacts, artifact, + hidePythonExecution, }: MessageProps) => { const { t } = useTranslation(); @@ -365,7 +367,9 @@ const MessageBase = ({ )} - {pythonCode && } + {pythonCode && !hidePythonExecution && ( + + )} {message.role === 'user' && !!message.attachments.length && ( -
+
{displayLabel && (