✨ feat: Assistants API, General File Support, Side Panel, File Explorer (#1696)
* feat: assistant name/icon in Landing & Header
* feat: assistname in textarea placeholder, and use `Assistant` as default name
* feat: display non-image files in user messages
* fix: only render files if files.length is > 0
* refactor(config -> file-config): move file related configuration values to separate module, add excel types
* chore: spreadsheet file rendering
* fix(Landing): dark mode style for Assistant Name
* refactor: move progress incrementing to own hook, start smaller, cap near limit \(1\)
* refactor(useContentHandler): add empty Text part if last part was completed tool or image
* chore: add accordion trigger border styling for dark mode
* feat: Assistant Builder model selection
* chore: use Spinner when Assistant is mutating
* fix(get/assistants): return correct response object `AssistantListResponse`
* refactor(Spinner): pass size as prop
* refactor: make assistant crud mutations optimistic, add types for options
* chore: remove assistants route and view
* chore: move assistant builder components to separate directory
* feat(ContextButton): delete Assistant via context button/dialog, add localization
* refactor: conditionally show use and context menu buttons, add localization for create assistant
* feat: save side panel states to localStorage
* style(SidePanel): improve avatar menu and assistant select styling for dark mode
* refactor: make NavToggle reusable for either side (left or right), add SidePanel Toggle with ability to close it completely
* fix: resize handle and navToggle behavior
* fix(/avatar/:assistant_id): await `deleteFile` and assign unique name to uploaded image
* WIP: file UI components from PR #576
* refactor(OpenAIMinimalIcon): pass className
* feat: formatDate helper fn
* feat: DataTableColumnHeader
* feat: add row selection, formatted row values, number of rows selected
* WIP: add files to Side panel temporarily
* feat: `LB_QueueAsyncCall`: Leaky Bucket queue for external APIs, use in `processDeleteRequest`
* fix(TFile): correct `source` type with `FileSources`
* fix(useFileHandling): use `continue` instead of return when iterating multiple files, add file type to extendedFile
* chore: add generic setter type
* refactor(processDeleteRequest): settle promises to prevent rejections from processing deletions, log errors
* feat: `useFileDeletion` to reuse file deletion logic
* refactor(useFileDeletion): make `setFiles` an optional param and use object as param
* feat: useDeleteFilesFromTable
* feat: use real `files` data and add deletion action to data table
* fix(Table): make headers sticky
* feat: add dynamic filtering for columns; only show to user Host or OpenAI storage type
* style(DropdownMenu): replace `slate` with `gray`
* style(DataTable): apply dark mode themes and other misc styling
* style(Columns): add color to OpenAI Storage option
* refactor(FileContainer): make file preview reusable
* refactor(Images): make image preview reusable
* refactor(FilePreview): make file prop optional for FileIcon and FilePreview, fix relative style
* feat(Columns): add file/image previews, set a minimum size to show for file size in bytes
* WIP: File Panel with real files and formatted
* feat: open files dialog from panel
* style: file data table mobile and general column styling fixes
* refactor(api/files): return files sorted by the most recently updated
* refactor: provide fileMap through context to prevent re-selecting files to map in different areas; remove unused imports commented out in PanelColumns
* refactor(ExtendFile): make File type optional, add `attached` to prevent attached files from being deleted on remove, make Message.files a partial TFile type
* feat: attach files through file panel
* refactor(useFileHandling): move files to the start of cache list when uploaded
* refactor(useDeleteFilesMutation): delete files from cache when successfully deleted from server
* fix(FileRow): handle possible edge case of duplication due to attaching recently uploaded file
* style(SidePanel): make resize grip border transparent, remove unnecessary styling on close sidepanel button
* feat: action utilities and tests
* refactor(actions): add `ValidationResult` type and change wording for no server URL found
* refactor(actions): check for empty server URL
* fix(data-provider): revert tsconfig to fix type issue resolution
* feat(client): first pass of actions input for assistants
* refactor(FunctionSignature): change method to output object instead of string
* refactor(models/Assistant): add actions field to schema, use searchParams object for methods, and add `getAssistant`
* feat: post actions input first pass
- create new Action document
- add actions to Assistant DB document
- create /action/:assistant_id POST route
- pass more props down from PanelSwitcher, derive assistant_id from switcher
- move privacy policy to ActionInput
- reset data on input change/validation
- add `useUpdateAction`
- conform FunctionSignature type to FunctionTool
- add action, assistant doc, update hook related types
* refactor: optimize assistant/actions relationship
- past domain in metadata as hostname and not a URL
- include domain in tool name
- add `getActions` for actions retrieval by user
- add `getAssistants` for assistant docs retrieval by user
- add `assistant_id` to Action schema
- move actions to own module as a subroute to `api/assistants`
- add `useGetActionsQuery` and `useGetAssistantDocsQuery` hooks
- fix Action type def
* feat: show assistant actions in assistant builder
* feat: switch to actions on action click, editing action styling
* fix: add Assistant state for builder panel to allow immediate selection of newly created assistants as well as retaining the current assistant when switching to a different panel within the builder
* refactor(SidePanel/NavToggle): offset less from right when SidePanel is completely collapsed
* chore: rename `processActions` -> `processRequiredActions`
* chore: rename Assistant API Action to RequiredAction
* refactor(actions): avoid nesting actual API params under generic `requestBody` to optimize LLM token usage
* fix(handleTools): avoid calling `validTool` if not defined, add optional param to skip the loading of specs, which throws an error in the context of assistants
* WIP: working first pass of toolCalls generated from openapi specs
* WIP: first pass ToolCall styling
* feat: programmatic iv encryption/decryption helpers
* fix: correct ActionAuth types/enums, and define type for AuthForm
* feat: encryption/decryption helpers for Action AuthMetadata
* refactor(getActions): remove sensitive fields from query response
* refactor(POST/actions): encrypt and remove sensitive fields from mutation response
* fix(ActionService): change ESM import to CJS
* feat: frontend auth handling for actions + optimistic update on action update/creation
* refactor(actions): use the correct variables and types for setAuth method
* refactor: POST /:assistant_id action can now handle updating an existing action, add `saved_auth_fields` to determine when user explicitly saves new auth creds. only send auth metadata if user explicitly saved fields
* refactor(createActionTool): catch errors and send back meaningful error message, add flag to `getActions` to determine whether to retrieve sensitive values or not
* refactor(ToolService): add `action` property to ToolCall PartMetadata to determine if the tool call was an action, fix parsing function name issue with actionDelimiter
* fix(ActionRequest): use URL class to correctly join endpoint parts for `execute` call
* feat: delete assistant actions
* refactor: conditionally show Available actions
* refactor: show `retrieval` and `code_interpreter` as Capabilities, swap `Switch` for `Checkbox`
* chore: remove shadow-stroke from messages
* WIP: first pass of Assistants Knowledge attachments
* refactor: remove AssistantsProvider in favor of FormProvider, fix selectedAssistant re-render bug, map Assistant file_ids to files via fileMap, initialize Knowledge component with mapped files if any exist
* fix: prevent deleting files on assistant file upload
* chore: remove console.log
* refactor(useUploadFileMutation): update files and assistants cache on upload
* chore: disable oauth option as not supported yet
* feat: cancel assistant runs
* refactor: initialize OpenAI client with helper function, resolve all related circular dependencies
* fix(DALL-E): initialization
* fix(process): openai client initialization
* fix: select an existing Assistant when the active one is deleted
* chore: allow attaching files for assistant endpoint, send back relevant OpenAI error message when uploading, deconstruct openAI initialization correctly, add `message_file` to formData when a file is attached to the message but not the assistant
* fix: add assistant_id on newConvo
* fix(initializeClient): import fix
* chore: swap setAssistant for setOption in useEffect
* fix(DALL-E): add processFileURL to loadTools call
* chore: add customConfig to debug logs
* feat: delete threads on convo delete
* chore: replace Assistants icon
* chore: remove console.dir() in `abortRun`
* feat(AssistantService): accumulate text values from run in openai.responseText
* feat: titling for assistants endpoint
* chore: move panel file components to appropriate directory, add file checks for attaching files, change icon for Attach Files
* refactor: add localizations to tools, plugins, add condition for adding/remove user plugins so tool selections don't affect this value
* chore: disable `import from url` action for now
* chore: remove textMimeTypes from default fileConfig for now
* fix: catch tool errors and send as outputs with error messages
* fix: React warning about button as descendant of button
* style: retrieval and cancelled icon
* WIP: pass isSubmitting to Parts, use InProgressCall to display cancelled tool calls correctly, show domain/function name
* fix(meilisearch): fix `postSaveHook` issue where indexing expects a mongo document, and join all text content parts for meili indexing
* ci: fix dall-e tests
* ci: fix client tests
* fix: button types in actions panel
* fix: plugin auth form persisting across tool selections
* fix(ci): update AppService spec with `loadAndFormatTools`
* fix(clearConvos): add id check earlier on
* refactor(AssistantAvatar): set previewURL dynamically when emtadata.avatar changes
* feat(assistants): addTitle cache setting
* fix(useSSE): resolve rebase conflicts
* fix: delete mutation
* style(SidePanel): make grip visible on active and hover, invisible otherwise
* ci: add data-provider tests to workflow, also update eslint/tsconfig to recognize specs, and add `text/csv` to fileConfig
* fix: handle edge case where auth object is undefined, and log errors
* refactor(actions): resolve schemas, add tests for resolving refs, import specs from separate file for tests
* chore: remove comment
* fix(ActionsInput): re-render bug when initializing states with action fields
* fix(patch/assistant): filter undefined tools
* chore: add logging for errors in assistants routes
* fix(updateAssistant): map actions to functions to avoid overwriting
* fix(actions): properly handle GET paths
* fix(convos): unhandled delete thread exception
* refactor(AssistantService): pass both thread_id and conversationId when sending intermediate assistant messages, remove `mapMessagesToSteps` from AssistantService
* refactor(useSSE): replace all messages with runMessages and pass latestMessageId to abortRun; fix(checkMessageGaps): include tool calls when syncing messages
* refactor(assistants/chat): invoke `createOnTextProgress` after thread creation
* chore: add typing
* style: sidepanel styling
* style: action tool call domain styling
* feat(assistants): default models, limit retrieval to certain models, add env variables to to env.example
* feat: assistants api key in EndpointService
* refactor: set assistant model to conversation on assistant switch
* refactor: set assistant model to conversation on assistant select from panel
* fix(retrieveAndProcessFile): catch attempt to download file with `assistant` purpose which is not allowed; add logging
* feat: retrieval styling, handling, and logging
* chore: rename ASSISTANTS_REVERSE_PROXY to ASSISTANTS_BASE_URL
* feat: FileContext for file metadata
* feat: context file mgmt and filtering
* style(Select): hover/rounded changes
* refactor: explicit conversation switch, endpoint dependent, through `useSelectAssistant`, which does not create new chat if current endpoint is assistant endpoint
* fix(AssistantAvatar): make empty previewURL if no avatar present
* refactor: side panel mobile styling
* style: merge tool and action section, optimize mobile styling for action/tool buttons
* fix: localStorage issues
* fix(useSelectAssistant): invoke react query hook directly in select hook as Map was not being updated in time
* style: light mode fixes
* fix: prevent sidepanel nav styling from shifting layout up
* refactor: change default layout (collapsed by default)
* style: mobile optimization of DataTable
* style: datatable
* feat: client-side hide right-side panel
* chore(useNewConvo): add partial typing for preset
* fix(useSelectAssistant): pass correct model name by using template as preset
* WIP: assistant presets
* refactor(ToolService): add native solution for `TavilySearchResults` and log tool output errors
* refactor: organize imports and use native TavilySearchResults
* fix(TavilySearchResults): stringify result
* fix(ToolCall): show tool call outputs when not an action
* chore: rename Prompt Prefix to custom instructions (in user facing text only)
* refactor(EditPresetDialog): Optimize setting title by debouncing, reset preset on dialog close to avoid state mixture
* feat: add `presetOverride` to overwrite active conversation settings when saving a Preset (relevant for client side updates only)
* feat: Assistant preset settings (client-side)
* fix(Switcher): only set assistant_id and model if current endpoint is Assistants
* feat: use `useDebouncedInput` for updating conversation settings, starting with EditPresetDialog title setting and Assistant instructions setting
* feat(Assistants): add instructions field to settings
* feat(chat/assistants): pass conversation settings to run body
* wip: begin localization and only allow actions if the assistant is created
* refactor(AssistantsPanel): knowledge localization, allow tools on creation
* feat: experimental: allow 'priming' values before assistant is created, that would normally require an assistant_id to be defined
* chore: trim console logs and make more meaningful
* chore: toast messages
* fix(ci): date test
* feat: create file when uploading Assistant Avatar
* feat: file upload rate limiting from custom config with dynamic file route initialization
* refactor: use file upload limiters on post routes only
* refactor(fileConfig): add endpoints field for endpoint specific fileconfigs, add mergeConfig function, add tests
* refactor: fileConfig route, dynamic multer instances used on all '/' and '/images' POST routes, data service and query hook
* feat: supportedMimeTypesSchema, test for array of regex
* feat: configurable file config limits
* chore: clarify assistants file knowledge prereq.
* chore(useTextarea): default to localized 'Assistant' if assistant name is empty
* feat: configurable file limits and toggle file upload per endpoint
* fix(useUploadFileMutation): prevent updating assistant.files cache if file upload is a message_file attachment
* fix(AssistantSelect): set last selected assistant only when timeout successfully runs
* refactor(queries): disable assistant queries if assistants endpoint is not enabled
* chore(Switcher): add localization
* chore: pluralize `assistant` for `EModelEndpoint key and value
* feat: show/hide assistant UI components based on endpoint availability; librechat.yaml config for disabling builder section and setting polling/timeout intervals
* fix(compactEndpointSchemas): use EModelEndpoint for schema access
* feat(runAssistant): use configured values from `librechat.yaml` for `pollIntervalMs` and `timeout`
* fix: naming issue
* wip: revert landing
* 🎉 happy birthday LibreChat (#1768)
* happy birthday LibreChat
* Refactor endpoint condition in Landing component
* Update birthday message in Eng.tsx
* fix(/config): avoid nesting ternaries
* refactor(/config): check birthday
---------
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
* fix: landing
* fix: landing
* fix(useMessageHelpers): hardcoded check to use EModelEndpoint instead
* fix(ci): convo test revert to main
* fix(assistants/chat): fix issue where assistant_id was being saved as model for convo
* chore: added logging, promises racing to prevent longer timeouts, explicit setting of maxRetries and timeouts, robust catching of invalid abortRun params
* refactor: use recoil state for `showStopButton` and only show for assistants endpoint after syncing conversation data
* refactor: optimize abortRun strategy using localStorage, refactor `abortConversation` to use async/await and await the result, refactor how the abortKey cache is set for runs
* fix(checkMessageGaps): assign `assistant_id` to synced messages if defined; prevents UI from showing blank assistant for cancelled messages
* refactor: re-order sequence of chat route, only allow aborting messages after run is created, cancel abortRun if there was a cancelling error (likely due already cancelled in chat route), and add extra logging
* chore(typedefs): add httpAgent type to OpenAIClient
* refactor: use custom implementation of retrieving run with axios to allow for timing out run query
* fix(waitForRun): handle timed out run retrieval query
* refactor: update preset conditions:
- presets will retain settings when a different endpoint is selected; for existing convos, either when modular or is assistant switch
- no longer use `navigateToConvo` on preset select
* fix: temporary calculator hack as expects string input when invoked
* fix: cancel abortRun only when cancelling error is a result of the run already being cancelled
* chore: remove use of `fileMaxSizeMB` and total counterpart (redundant)
* docs: custom config documentation update
* docs: assistants api setup and dotenv, new custom config fields
* refactor(Switcher): make Assistant switcher sticky in SidePanel
* chore(useSSE): remove console log of data and message index
* refactor(AssistantPanel): button styling and add secondary select button to bottom of panel
* refactor(OpenAIClient): allow passing conversationId to RunManager through titleConvo and initializeLLM to properly record title context tokens used in cases where conversationId was not defined by the client
* feat(assistants): token tracking for assistant runs
* chore(spendTokens): improve logging
* feat: support/exclude specific assistant Ids
* chore: add update `librechat.example.yaml`, optimize `AppService` handling, new tests for `AppService`, optimize missing/outdate config logging
* chore: mount docker logs to root of project
* chore: condense axios errors
* chore: bump vite
* chore: vite hot reload fix using latest version
* chore(getOpenAIModels): sort instruct models to the end of models list
* fix(assistants): user provided key
* fix(assistants): user provided key, invalidate more queries on revoke
---------
Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
2024-02-13 20:42:27 -05:00
|
|
|
import axios from 'axios';
|
|
|
|
|
import { OpenAPIV3 } from 'openapi-types';
|
|
|
|
|
import {
|
|
|
|
|
resolveRef,
|
|
|
|
|
ActionRequest,
|
|
|
|
|
openapiToFunction,
|
|
|
|
|
FunctionSignature,
|
|
|
|
|
validateAndParseOpenAPISpec,
|
|
|
|
|
} from '../src/actions';
|
|
|
|
|
import { getWeatherOpenapiSpec, whimsicalOpenapiSpec, scholarAIOpenapiSpec } from './openapiSpecs';
|
|
|
|
|
import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants';
|
|
|
|
|
import type { FlowchartSchema } from './openapiSpecs';
|
|
|
|
|
import type { ParametersSchema } from '../src/actions';
|
|
|
|
|
|
|
|
|
|
jest.mock('axios');
|
|
|
|
|
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
|
|
|
|
|
|
|
|
describe('FunctionSignature', () => {
|
|
|
|
|
it('creates a function signature and converts to JSON tool', () => {
|
|
|
|
|
const signature = new FunctionSignature('testFunction', 'A test function', {
|
|
|
|
|
param1: { type: 'string' },
|
|
|
|
|
} as unknown as ParametersSchema);
|
|
|
|
|
expect(signature.name).toBe('testFunction');
|
|
|
|
|
expect(signature.description).toBe('A test function');
|
|
|
|
|
expect(signature.toObjectTool()).toEqual({
|
|
|
|
|
type: 'function',
|
|
|
|
|
function: {
|
|
|
|
|
name: 'testFunction',
|
|
|
|
|
description: 'A test function',
|
|
|
|
|
parameters: {
|
|
|
|
|
param1: { type: 'string' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('ActionRequest', () => {
|
|
|
|
|
// Mocking responses for each method
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mockedAxios.get.mockResolvedValue({ data: { success: true, method: 'GET' } });
|
|
|
|
|
mockedAxios.post.mockResolvedValue({ data: { success: true, method: 'POST' } });
|
|
|
|
|
mockedAxios.put.mockResolvedValue({ data: { success: true, method: 'PUT' } });
|
|
|
|
|
mockedAxios.delete.mockResolvedValue({ data: { success: true, method: 'DELETE' } });
|
|
|
|
|
mockedAxios.patch.mockResolvedValue({ data: { success: true, method: 'PATCH' } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should make a GET request', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/test',
|
|
|
|
|
'GET',
|
|
|
|
|
'testOp',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param1: 'value1' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', expect.anything());
|
|
|
|
|
expect(response.data).toEqual({ success: true, method: 'GET' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('ActionRequest', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mockedAxios.get.mockClear();
|
|
|
|
|
mockedAxios.post.mockClear();
|
|
|
|
|
mockedAxios.put.mockClear();
|
|
|
|
|
mockedAxios.delete.mockClear();
|
|
|
|
|
mockedAxios.patch.mockClear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles GET requests', async () => {
|
|
|
|
|
mockedAxios.get.mockResolvedValue({ data: { success: true } });
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/get',
|
|
|
|
|
'GET',
|
|
|
|
|
'testGet',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param: 'test' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalled();
|
|
|
|
|
expect(response.data.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles POST requests', async () => {
|
|
|
|
|
mockedAxios.post.mockResolvedValue({ data: { success: true } });
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/post',
|
|
|
|
|
'POST',
|
|
|
|
|
'testPost',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param: 'test' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.post).toHaveBeenCalled();
|
|
|
|
|
expect(response.data.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles PUT requests', async () => {
|
|
|
|
|
mockedAxios.put.mockResolvedValue({ data: { success: true } });
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/put',
|
|
|
|
|
'PUT',
|
|
|
|
|
'testPut',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param: 'test' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.put).toHaveBeenCalled();
|
|
|
|
|
expect(response.data.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles DELETE requests', async () => {
|
|
|
|
|
mockedAxios.delete.mockResolvedValue({ data: { success: true } });
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/delete',
|
|
|
|
|
'DELETE',
|
|
|
|
|
'testDelete',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param: 'test' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.delete).toHaveBeenCalled();
|
|
|
|
|
expect(response.data.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles PATCH requests', async () => {
|
|
|
|
|
mockedAxios.patch.mockResolvedValue({ data: { success: true } });
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/patch',
|
|
|
|
|
'PATCH',
|
|
|
|
|
'testPatch',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await actionRequest.setParams({ param: 'test' });
|
|
|
|
|
const response = await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
|
|
|
expect(response.data.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws an error for unsupported HTTP methods', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/invalid',
|
|
|
|
|
'INVALID',
|
|
|
|
|
'testInvalid',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
|
|
|
|
});
|
2024-03-16 16:49:11 -04:00
|
|
|
|
|
|
|
|
it('replaces path parameters with values from toolInput', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/stocks/{stocksTicker}/bars/{multiplier}',
|
|
|
|
|
'GET',
|
|
|
|
|
'getAggregateBars',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await actionRequest.setParams({
|
|
|
|
|
stocksTicker: 'AAPL',
|
|
|
|
|
multiplier: 5,
|
|
|
|
|
startDate: '2023-01-01',
|
|
|
|
|
endDate: '2023-12-31',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(actionRequest.path).toBe('/stocks/AAPL/bars/5');
|
|
|
|
|
expect(actionRequest.params).toEqual({
|
|
|
|
|
startDate: '2023-01-01',
|
|
|
|
|
endDate: '2023-12-31',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/stocks/AAPL/bars/5', {
|
|
|
|
|
headers: expect.anything(),
|
|
|
|
|
params: {
|
|
|
|
|
startDate: '2023-01-01',
|
|
|
|
|
endDate: '2023-12-31',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
✨ feat: Assistants API, General File Support, Side Panel, File Explorer (#1696)
* feat: assistant name/icon in Landing & Header
* feat: assistname in textarea placeholder, and use `Assistant` as default name
* feat: display non-image files in user messages
* fix: only render files if files.length is > 0
* refactor(config -> file-config): move file related configuration values to separate module, add excel types
* chore: spreadsheet file rendering
* fix(Landing): dark mode style for Assistant Name
* refactor: move progress incrementing to own hook, start smaller, cap near limit \(1\)
* refactor(useContentHandler): add empty Text part if last part was completed tool or image
* chore: add accordion trigger border styling for dark mode
* feat: Assistant Builder model selection
* chore: use Spinner when Assistant is mutating
* fix(get/assistants): return correct response object `AssistantListResponse`
* refactor(Spinner): pass size as prop
* refactor: make assistant crud mutations optimistic, add types for options
* chore: remove assistants route and view
* chore: move assistant builder components to separate directory
* feat(ContextButton): delete Assistant via context button/dialog, add localization
* refactor: conditionally show use and context menu buttons, add localization for create assistant
* feat: save side panel states to localStorage
* style(SidePanel): improve avatar menu and assistant select styling for dark mode
* refactor: make NavToggle reusable for either side (left or right), add SidePanel Toggle with ability to close it completely
* fix: resize handle and navToggle behavior
* fix(/avatar/:assistant_id): await `deleteFile` and assign unique name to uploaded image
* WIP: file UI components from PR #576
* refactor(OpenAIMinimalIcon): pass className
* feat: formatDate helper fn
* feat: DataTableColumnHeader
* feat: add row selection, formatted row values, number of rows selected
* WIP: add files to Side panel temporarily
* feat: `LB_QueueAsyncCall`: Leaky Bucket queue for external APIs, use in `processDeleteRequest`
* fix(TFile): correct `source` type with `FileSources`
* fix(useFileHandling): use `continue` instead of return when iterating multiple files, add file type to extendedFile
* chore: add generic setter type
* refactor(processDeleteRequest): settle promises to prevent rejections from processing deletions, log errors
* feat: `useFileDeletion` to reuse file deletion logic
* refactor(useFileDeletion): make `setFiles` an optional param and use object as param
* feat: useDeleteFilesFromTable
* feat: use real `files` data and add deletion action to data table
* fix(Table): make headers sticky
* feat: add dynamic filtering for columns; only show to user Host or OpenAI storage type
* style(DropdownMenu): replace `slate` with `gray`
* style(DataTable): apply dark mode themes and other misc styling
* style(Columns): add color to OpenAI Storage option
* refactor(FileContainer): make file preview reusable
* refactor(Images): make image preview reusable
* refactor(FilePreview): make file prop optional for FileIcon and FilePreview, fix relative style
* feat(Columns): add file/image previews, set a minimum size to show for file size in bytes
* WIP: File Panel with real files and formatted
* feat: open files dialog from panel
* style: file data table mobile and general column styling fixes
* refactor(api/files): return files sorted by the most recently updated
* refactor: provide fileMap through context to prevent re-selecting files to map in different areas; remove unused imports commented out in PanelColumns
* refactor(ExtendFile): make File type optional, add `attached` to prevent attached files from being deleted on remove, make Message.files a partial TFile type
* feat: attach files through file panel
* refactor(useFileHandling): move files to the start of cache list when uploaded
* refactor(useDeleteFilesMutation): delete files from cache when successfully deleted from server
* fix(FileRow): handle possible edge case of duplication due to attaching recently uploaded file
* style(SidePanel): make resize grip border transparent, remove unnecessary styling on close sidepanel button
* feat: action utilities and tests
* refactor(actions): add `ValidationResult` type and change wording for no server URL found
* refactor(actions): check for empty server URL
* fix(data-provider): revert tsconfig to fix type issue resolution
* feat(client): first pass of actions input for assistants
* refactor(FunctionSignature): change method to output object instead of string
* refactor(models/Assistant): add actions field to schema, use searchParams object for methods, and add `getAssistant`
* feat: post actions input first pass
- create new Action document
- add actions to Assistant DB document
- create /action/:assistant_id POST route
- pass more props down from PanelSwitcher, derive assistant_id from switcher
- move privacy policy to ActionInput
- reset data on input change/validation
- add `useUpdateAction`
- conform FunctionSignature type to FunctionTool
- add action, assistant doc, update hook related types
* refactor: optimize assistant/actions relationship
- past domain in metadata as hostname and not a URL
- include domain in tool name
- add `getActions` for actions retrieval by user
- add `getAssistants` for assistant docs retrieval by user
- add `assistant_id` to Action schema
- move actions to own module as a subroute to `api/assistants`
- add `useGetActionsQuery` and `useGetAssistantDocsQuery` hooks
- fix Action type def
* feat: show assistant actions in assistant builder
* feat: switch to actions on action click, editing action styling
* fix: add Assistant state for builder panel to allow immediate selection of newly created assistants as well as retaining the current assistant when switching to a different panel within the builder
* refactor(SidePanel/NavToggle): offset less from right when SidePanel is completely collapsed
* chore: rename `processActions` -> `processRequiredActions`
* chore: rename Assistant API Action to RequiredAction
* refactor(actions): avoid nesting actual API params under generic `requestBody` to optimize LLM token usage
* fix(handleTools): avoid calling `validTool` if not defined, add optional param to skip the loading of specs, which throws an error in the context of assistants
* WIP: working first pass of toolCalls generated from openapi specs
* WIP: first pass ToolCall styling
* feat: programmatic iv encryption/decryption helpers
* fix: correct ActionAuth types/enums, and define type for AuthForm
* feat: encryption/decryption helpers for Action AuthMetadata
* refactor(getActions): remove sensitive fields from query response
* refactor(POST/actions): encrypt and remove sensitive fields from mutation response
* fix(ActionService): change ESM import to CJS
* feat: frontend auth handling for actions + optimistic update on action update/creation
* refactor(actions): use the correct variables and types for setAuth method
* refactor: POST /:assistant_id action can now handle updating an existing action, add `saved_auth_fields` to determine when user explicitly saves new auth creds. only send auth metadata if user explicitly saved fields
* refactor(createActionTool): catch errors and send back meaningful error message, add flag to `getActions` to determine whether to retrieve sensitive values or not
* refactor(ToolService): add `action` property to ToolCall PartMetadata to determine if the tool call was an action, fix parsing function name issue with actionDelimiter
* fix(ActionRequest): use URL class to correctly join endpoint parts for `execute` call
* feat: delete assistant actions
* refactor: conditionally show Available actions
* refactor: show `retrieval` and `code_interpreter` as Capabilities, swap `Switch` for `Checkbox`
* chore: remove shadow-stroke from messages
* WIP: first pass of Assistants Knowledge attachments
* refactor: remove AssistantsProvider in favor of FormProvider, fix selectedAssistant re-render bug, map Assistant file_ids to files via fileMap, initialize Knowledge component with mapped files if any exist
* fix: prevent deleting files on assistant file upload
* chore: remove console.log
* refactor(useUploadFileMutation): update files and assistants cache on upload
* chore: disable oauth option as not supported yet
* feat: cancel assistant runs
* refactor: initialize OpenAI client with helper function, resolve all related circular dependencies
* fix(DALL-E): initialization
* fix(process): openai client initialization
* fix: select an existing Assistant when the active one is deleted
* chore: allow attaching files for assistant endpoint, send back relevant OpenAI error message when uploading, deconstruct openAI initialization correctly, add `message_file` to formData when a file is attached to the message but not the assistant
* fix: add assistant_id on newConvo
* fix(initializeClient): import fix
* chore: swap setAssistant for setOption in useEffect
* fix(DALL-E): add processFileURL to loadTools call
* chore: add customConfig to debug logs
* feat: delete threads on convo delete
* chore: replace Assistants icon
* chore: remove console.dir() in `abortRun`
* feat(AssistantService): accumulate text values from run in openai.responseText
* feat: titling for assistants endpoint
* chore: move panel file components to appropriate directory, add file checks for attaching files, change icon for Attach Files
* refactor: add localizations to tools, plugins, add condition for adding/remove user plugins so tool selections don't affect this value
* chore: disable `import from url` action for now
* chore: remove textMimeTypes from default fileConfig for now
* fix: catch tool errors and send as outputs with error messages
* fix: React warning about button as descendant of button
* style: retrieval and cancelled icon
* WIP: pass isSubmitting to Parts, use InProgressCall to display cancelled tool calls correctly, show domain/function name
* fix(meilisearch): fix `postSaveHook` issue where indexing expects a mongo document, and join all text content parts for meili indexing
* ci: fix dall-e tests
* ci: fix client tests
* fix: button types in actions panel
* fix: plugin auth form persisting across tool selections
* fix(ci): update AppService spec with `loadAndFormatTools`
* fix(clearConvos): add id check earlier on
* refactor(AssistantAvatar): set previewURL dynamically when emtadata.avatar changes
* feat(assistants): addTitle cache setting
* fix(useSSE): resolve rebase conflicts
* fix: delete mutation
* style(SidePanel): make grip visible on active and hover, invisible otherwise
* ci: add data-provider tests to workflow, also update eslint/tsconfig to recognize specs, and add `text/csv` to fileConfig
* fix: handle edge case where auth object is undefined, and log errors
* refactor(actions): resolve schemas, add tests for resolving refs, import specs from separate file for tests
* chore: remove comment
* fix(ActionsInput): re-render bug when initializing states with action fields
* fix(patch/assistant): filter undefined tools
* chore: add logging for errors in assistants routes
* fix(updateAssistant): map actions to functions to avoid overwriting
* fix(actions): properly handle GET paths
* fix(convos): unhandled delete thread exception
* refactor(AssistantService): pass both thread_id and conversationId when sending intermediate assistant messages, remove `mapMessagesToSteps` from AssistantService
* refactor(useSSE): replace all messages with runMessages and pass latestMessageId to abortRun; fix(checkMessageGaps): include tool calls when syncing messages
* refactor(assistants/chat): invoke `createOnTextProgress` after thread creation
* chore: add typing
* style: sidepanel styling
* style: action tool call domain styling
* feat(assistants): default models, limit retrieval to certain models, add env variables to to env.example
* feat: assistants api key in EndpointService
* refactor: set assistant model to conversation on assistant switch
* refactor: set assistant model to conversation on assistant select from panel
* fix(retrieveAndProcessFile): catch attempt to download file with `assistant` purpose which is not allowed; add logging
* feat: retrieval styling, handling, and logging
* chore: rename ASSISTANTS_REVERSE_PROXY to ASSISTANTS_BASE_URL
* feat: FileContext for file metadata
* feat: context file mgmt and filtering
* style(Select): hover/rounded changes
* refactor: explicit conversation switch, endpoint dependent, through `useSelectAssistant`, which does not create new chat if current endpoint is assistant endpoint
* fix(AssistantAvatar): make empty previewURL if no avatar present
* refactor: side panel mobile styling
* style: merge tool and action section, optimize mobile styling for action/tool buttons
* fix: localStorage issues
* fix(useSelectAssistant): invoke react query hook directly in select hook as Map was not being updated in time
* style: light mode fixes
* fix: prevent sidepanel nav styling from shifting layout up
* refactor: change default layout (collapsed by default)
* style: mobile optimization of DataTable
* style: datatable
* feat: client-side hide right-side panel
* chore(useNewConvo): add partial typing for preset
* fix(useSelectAssistant): pass correct model name by using template as preset
* WIP: assistant presets
* refactor(ToolService): add native solution for `TavilySearchResults` and log tool output errors
* refactor: organize imports and use native TavilySearchResults
* fix(TavilySearchResults): stringify result
* fix(ToolCall): show tool call outputs when not an action
* chore: rename Prompt Prefix to custom instructions (in user facing text only)
* refactor(EditPresetDialog): Optimize setting title by debouncing, reset preset on dialog close to avoid state mixture
* feat: add `presetOverride` to overwrite active conversation settings when saving a Preset (relevant for client side updates only)
* feat: Assistant preset settings (client-side)
* fix(Switcher): only set assistant_id and model if current endpoint is Assistants
* feat: use `useDebouncedInput` for updating conversation settings, starting with EditPresetDialog title setting and Assistant instructions setting
* feat(Assistants): add instructions field to settings
* feat(chat/assistants): pass conversation settings to run body
* wip: begin localization and only allow actions if the assistant is created
* refactor(AssistantsPanel): knowledge localization, allow tools on creation
* feat: experimental: allow 'priming' values before assistant is created, that would normally require an assistant_id to be defined
* chore: trim console logs and make more meaningful
* chore: toast messages
* fix(ci): date test
* feat: create file when uploading Assistant Avatar
* feat: file upload rate limiting from custom config with dynamic file route initialization
* refactor: use file upload limiters on post routes only
* refactor(fileConfig): add endpoints field for endpoint specific fileconfigs, add mergeConfig function, add tests
* refactor: fileConfig route, dynamic multer instances used on all '/' and '/images' POST routes, data service and query hook
* feat: supportedMimeTypesSchema, test for array of regex
* feat: configurable file config limits
* chore: clarify assistants file knowledge prereq.
* chore(useTextarea): default to localized 'Assistant' if assistant name is empty
* feat: configurable file limits and toggle file upload per endpoint
* fix(useUploadFileMutation): prevent updating assistant.files cache if file upload is a message_file attachment
* fix(AssistantSelect): set last selected assistant only when timeout successfully runs
* refactor(queries): disable assistant queries if assistants endpoint is not enabled
* chore(Switcher): add localization
* chore: pluralize `assistant` for `EModelEndpoint key and value
* feat: show/hide assistant UI components based on endpoint availability; librechat.yaml config for disabling builder section and setting polling/timeout intervals
* fix(compactEndpointSchemas): use EModelEndpoint for schema access
* feat(runAssistant): use configured values from `librechat.yaml` for `pollIntervalMs` and `timeout`
* fix: naming issue
* wip: revert landing
* 🎉 happy birthday LibreChat (#1768)
* happy birthday LibreChat
* Refactor endpoint condition in Landing component
* Update birthday message in Eng.tsx
* fix(/config): avoid nesting ternaries
* refactor(/config): check birthday
---------
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
* fix: landing
* fix: landing
* fix(useMessageHelpers): hardcoded check to use EModelEndpoint instead
* fix(ci): convo test revert to main
* fix(assistants/chat): fix issue where assistant_id was being saved as model for convo
* chore: added logging, promises racing to prevent longer timeouts, explicit setting of maxRetries and timeouts, robust catching of invalid abortRun params
* refactor: use recoil state for `showStopButton` and only show for assistants endpoint after syncing conversation data
* refactor: optimize abortRun strategy using localStorage, refactor `abortConversation` to use async/await and await the result, refactor how the abortKey cache is set for runs
* fix(checkMessageGaps): assign `assistant_id` to synced messages if defined; prevents UI from showing blank assistant for cancelled messages
* refactor: re-order sequence of chat route, only allow aborting messages after run is created, cancel abortRun if there was a cancelling error (likely due already cancelled in chat route), and add extra logging
* chore(typedefs): add httpAgent type to OpenAIClient
* refactor: use custom implementation of retrieving run with axios to allow for timing out run query
* fix(waitForRun): handle timed out run retrieval query
* refactor: update preset conditions:
- presets will retain settings when a different endpoint is selected; for existing convos, either when modular or is assistant switch
- no longer use `navigateToConvo` on preset select
* fix: temporary calculator hack as expects string input when invoked
* fix: cancel abortRun only when cancelling error is a result of the run already being cancelled
* chore: remove use of `fileMaxSizeMB` and total counterpart (redundant)
* docs: custom config documentation update
* docs: assistants api setup and dotenv, new custom config fields
* refactor(Switcher): make Assistant switcher sticky in SidePanel
* chore(useSSE): remove console log of data and message index
* refactor(AssistantPanel): button styling and add secondary select button to bottom of panel
* refactor(OpenAIClient): allow passing conversationId to RunManager through titleConvo and initializeLLM to properly record title context tokens used in cases where conversationId was not defined by the client
* feat(assistants): token tracking for assistant runs
* chore(spendTokens): improve logging
* feat: support/exclude specific assistant Ids
* chore: add update `librechat.example.yaml`, optimize `AppService` handling, new tests for `AppService`, optimize missing/outdate config logging
* chore: mount docker logs to root of project
* chore: condense axios errors
* chore: bump vite
* chore: vite hot reload fix using latest version
* chore(getOpenAIModels): sort instruct models to the end of models list
* fix(assistants): user provided key
* fix(assistants): user provided key, invalidate more queries on revoke
---------
Co-authored-by: Marco Beretta <81851188+Berry-13@users.noreply.github.com>
2024-02-13 20:42:27 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws an error for unsupported HTTP method', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/test',
|
|
|
|
|
'INVALID',
|
|
|
|
|
'testOp',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Authentication Handling', () => {
|
|
|
|
|
it('correctly sets Basic Auth header', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/test',
|
|
|
|
|
'GET',
|
|
|
|
|
'testOp',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const api_key = 'user:pass';
|
|
|
|
|
const encodedCredentials = Buffer.from('user:pass').toString('base64');
|
|
|
|
|
|
|
|
|
|
actionRequest.setAuth({
|
|
|
|
|
auth: {
|
|
|
|
|
type: AuthTypeEnum.ServiceHttp,
|
|
|
|
|
authorization_type: AuthorizationTypeEnum.Basic,
|
|
|
|
|
},
|
|
|
|
|
api_key,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await actionRequest.setParams({ param1: 'value1' });
|
|
|
|
|
await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
|
|
|
headers: expect.objectContaining({
|
|
|
|
|
Authorization: `Basic ${encodedCredentials}`,
|
|
|
|
|
}),
|
|
|
|
|
params: expect.anything(),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('correctly sets Bearer token', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/test',
|
|
|
|
|
'GET',
|
|
|
|
|
'testOp',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
actionRequest.setAuth({
|
|
|
|
|
auth: {
|
|
|
|
|
type: AuthTypeEnum.ServiceHttp,
|
|
|
|
|
authorization_type: AuthorizationTypeEnum.Bearer,
|
|
|
|
|
},
|
|
|
|
|
api_key: 'token123',
|
|
|
|
|
});
|
|
|
|
|
await actionRequest.setParams({ param1: 'value1' });
|
|
|
|
|
await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
|
|
|
headers: expect.objectContaining({
|
|
|
|
|
Authorization: 'Bearer token123',
|
|
|
|
|
}),
|
|
|
|
|
params: expect.anything(),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('correctly sets API Key', async () => {
|
|
|
|
|
const actionRequest = new ActionRequest(
|
|
|
|
|
'https://example.com',
|
|
|
|
|
'/test',
|
|
|
|
|
'GET',
|
|
|
|
|
'testOp',
|
|
|
|
|
false,
|
|
|
|
|
'application/json',
|
|
|
|
|
);
|
|
|
|
|
// Updated to match ActionMetadata structure
|
|
|
|
|
actionRequest.setAuth({
|
|
|
|
|
auth: {
|
|
|
|
|
type: AuthTypeEnum.ServiceHttp, // Assuming this is a valid enum or value for your context
|
|
|
|
|
authorization_type: AuthorizationTypeEnum.Custom, // Assuming Custom means using a custom header
|
|
|
|
|
custom_auth_header: 'X-API-KEY',
|
|
|
|
|
},
|
|
|
|
|
api_key: 'abc123',
|
|
|
|
|
});
|
|
|
|
|
await actionRequest.setParams({ param1: 'value1' });
|
|
|
|
|
await actionRequest.execute();
|
|
|
|
|
expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', {
|
|
|
|
|
headers: expect.objectContaining({
|
|
|
|
|
'X-API-KEY': 'abc123',
|
|
|
|
|
}),
|
|
|
|
|
params: expect.anything(),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('resolveRef', () => {
|
|
|
|
|
it('correctly resolves $ref references in the OpenAPI spec', () => {
|
|
|
|
|
const openapiSpec = whimsicalOpenapiSpec;
|
|
|
|
|
const flowchartRequestRef = (
|
|
|
|
|
openapiSpec.paths['/ai.chatgpt.render-flowchart']?.post
|
|
|
|
|
?.requestBody as OpenAPIV3.RequestBodyObject
|
|
|
|
|
)?.content['application/json'].schema;
|
|
|
|
|
expect(flowchartRequestRef).toBeDefined();
|
|
|
|
|
const resolvedFlowchartRequest = resolveRef(
|
|
|
|
|
flowchartRequestRef as OpenAPIV3.RequestBodyObject,
|
|
|
|
|
openapiSpec.components,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(resolvedFlowchartRequest).toBeDefined();
|
|
|
|
|
expect(resolvedFlowchartRequest.type).toBe('object');
|
|
|
|
|
const properties = resolvedFlowchartRequest.properties as FlowchartSchema;
|
|
|
|
|
expect(properties).toBeDefined();
|
|
|
|
|
expect(properties.mermaid).toBeDefined();
|
|
|
|
|
expect(properties.mermaid.type).toBe('string');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('openapiToFunction', () => {
|
|
|
|
|
it('converts OpenAPI spec to function signatures and request builders', () => {
|
|
|
|
|
const { functionSignatures, requestBuilders } = openapiToFunction(getWeatherOpenapiSpec);
|
|
|
|
|
expect(functionSignatures.length).toBe(1);
|
|
|
|
|
expect(functionSignatures[0].name).toBe('GetCurrentWeather');
|
|
|
|
|
|
|
|
|
|
const parameters = functionSignatures[0].parameters as ParametersSchema & {
|
|
|
|
|
properties: {
|
|
|
|
|
location: {
|
|
|
|
|
type: 'string';
|
|
|
|
|
};
|
|
|
|
|
locations: {
|
|
|
|
|
type: 'array';
|
|
|
|
|
items: {
|
|
|
|
|
type: 'object';
|
|
|
|
|
properties: {
|
|
|
|
|
city: {
|
|
|
|
|
type: 'string';
|
|
|
|
|
};
|
|
|
|
|
state: {
|
|
|
|
|
type: 'string';
|
|
|
|
|
};
|
|
|
|
|
countryCode: {
|
|
|
|
|
type: 'string';
|
|
|
|
|
};
|
|
|
|
|
time: {
|
|
|
|
|
type: 'string';
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(parameters).toBeDefined();
|
|
|
|
|
expect(parameters.properties.locations).toBeDefined();
|
|
|
|
|
expect(parameters.properties.locations.type).toBe('array');
|
|
|
|
|
expect(parameters.properties.locations.items.type).toBe('object');
|
|
|
|
|
|
|
|
|
|
expect(parameters.properties.locations.items.properties.city.type).toBe('string');
|
|
|
|
|
expect(parameters.properties.locations.items.properties.state.type).toBe('string');
|
|
|
|
|
expect(parameters.properties.locations.items.properties.countryCode.type).toBe('string');
|
|
|
|
|
expect(parameters.properties.locations.items.properties.time.type).toBe('string');
|
|
|
|
|
|
|
|
|
|
expect(requestBuilders).toHaveProperty('GetCurrentWeather');
|
|
|
|
|
expect(requestBuilders.GetCurrentWeather).toBeInstanceOf(ActionRequest);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('openapiToFunction with $ref resolution', () => {
|
|
|
|
|
it('correctly converts OpenAPI spec to function signatures and request builders, resolving $ref references', () => {
|
|
|
|
|
const { functionSignatures, requestBuilders } = openapiToFunction(whimsicalOpenapiSpec);
|
|
|
|
|
|
|
|
|
|
expect(functionSignatures.length).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
const postRenderFlowchartSignature = functionSignatures.find(
|
|
|
|
|
(sig) => sig.name === 'postRenderFlowchart',
|
|
|
|
|
);
|
|
|
|
|
expect(postRenderFlowchartSignature).toBeDefined();
|
|
|
|
|
expect(postRenderFlowchartSignature?.name).toBe('postRenderFlowchart');
|
|
|
|
|
expect(postRenderFlowchartSignature?.parameters).toBeDefined();
|
|
|
|
|
|
|
|
|
|
expect(requestBuilders).toHaveProperty('postRenderFlowchart');
|
|
|
|
|
const postRenderFlowchartRequestBuilder = requestBuilders['postRenderFlowchart'];
|
|
|
|
|
expect(postRenderFlowchartRequestBuilder).toBeDefined();
|
|
|
|
|
expect(postRenderFlowchartRequestBuilder.method).toBe('post');
|
|
|
|
|
expect(postRenderFlowchartRequestBuilder.path).toBe('/ai.chatgpt.render-flowchart');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const invalidServerURL = 'Could not find a valid URL in `servers`';
|
|
|
|
|
|
|
|
|
|
describe('validateAndParseOpenAPISpec', () => {
|
|
|
|
|
it('validates a correct OpenAPI spec successfully', () => {
|
|
|
|
|
const validSpec = JSON.stringify({
|
|
|
|
|
openapi: '3.0.0',
|
|
|
|
|
info: { title: 'Test API', version: '1.0.0' },
|
|
|
|
|
servers: [{ url: 'https://test.api' }],
|
|
|
|
|
paths: { '/test': {} },
|
|
|
|
|
components: { schemas: {} },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(validSpec);
|
|
|
|
|
expect(result.status).toBe(true);
|
|
|
|
|
expect(result.message).toBe('OpenAPI spec is valid.');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns an error for spec with no servers', () => {
|
|
|
|
|
const noServerSpec = JSON.stringify({
|
|
|
|
|
openapi: '3.0.0',
|
|
|
|
|
info: { title: 'Test API', version: '1.0.0' },
|
|
|
|
|
paths: { '/test': {} },
|
|
|
|
|
components: { schemas: {} },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(noServerSpec);
|
|
|
|
|
expect(result.status).toBe(false);
|
|
|
|
|
expect(result.message).toBe(invalidServerURL);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns an error for spec with empty server URL', () => {
|
|
|
|
|
const emptyURLSpec = `{
|
|
|
|
|
"openapi": "3.1.0",
|
|
|
|
|
"info": {
|
|
|
|
|
"title": "Untitled",
|
|
|
|
|
"description": "Your OpenAPI specification",
|
|
|
|
|
"version": "v1.0.0"
|
|
|
|
|
},
|
|
|
|
|
"servers": [
|
|
|
|
|
{
|
|
|
|
|
"url": ""
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"paths": {},
|
|
|
|
|
"components": {
|
|
|
|
|
"schemas": {}
|
|
|
|
|
}
|
|
|
|
|
}`;
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(emptyURLSpec);
|
|
|
|
|
expect(result.status).toBe(false);
|
|
|
|
|
expect(result.message).toBe(invalidServerURL);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns an error for spec with no paths', () => {
|
|
|
|
|
const noPathsSpec = JSON.stringify({
|
|
|
|
|
openapi: '3.0.0',
|
|
|
|
|
info: { title: 'Test API', version: '1.0.0' },
|
|
|
|
|
servers: [{ url: 'https://test.api' }],
|
|
|
|
|
components: { schemas: {} },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(noPathsSpec);
|
|
|
|
|
expect(result.status).toBe(false);
|
|
|
|
|
expect(result.message).toBe('No paths found in the OpenAPI spec.');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('detects missing components in spec', () => {
|
|
|
|
|
const missingComponentSpec = JSON.stringify({
|
|
|
|
|
openapi: '3.0.0',
|
|
|
|
|
info: { title: 'Test API', version: '1.0.0' },
|
|
|
|
|
servers: [{ url: 'https://test.api' }],
|
|
|
|
|
paths: {
|
|
|
|
|
'/test': {
|
|
|
|
|
get: {
|
|
|
|
|
responses: {
|
|
|
|
|
'200': {
|
|
|
|
|
content: {
|
|
|
|
|
'application/json': { schema: { $ref: '#/components/schemas/Missing' } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(missingComponentSpec);
|
|
|
|
|
expect(result.status).toBe(true);
|
|
|
|
|
expect(result.message).toContain('reference to unknown component Missing');
|
|
|
|
|
expect(result.spec).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles invalid spec formats', () => {
|
|
|
|
|
const invalidSpec = 'not a valid spec';
|
|
|
|
|
|
|
|
|
|
const result = validateAndParseOpenAPISpec(invalidSpec);
|
|
|
|
|
expect(result.status).toBe(false);
|
|
|
|
|
expect(result.message).toBe(invalidServerURL);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles YAML spec and correctly converts to Function Signatures', () => {
|
|
|
|
|
const result = validateAndParseOpenAPISpec(scholarAIOpenapiSpec);
|
|
|
|
|
expect(result.status).toBe(true);
|
|
|
|
|
|
|
|
|
|
const spec = result.spec;
|
|
|
|
|
expect(spec).toBeDefined();
|
|
|
|
|
|
|
|
|
|
const { functionSignatures, requestBuilders } = openapiToFunction(spec as OpenAPIV3.Document);
|
|
|
|
|
expect(functionSignatures.length).toBe(3);
|
|
|
|
|
expect(requestBuilders).toHaveProperty('searchAbstracts');
|
|
|
|
|
expect(requestBuilders).toHaveProperty('getFullText');
|
|
|
|
|
expect(requestBuilders).toHaveProperty('saveCitation');
|
|
|
|
|
});
|
|
|
|
|
});
|