diff --git a/js/app/packages/block-md/component/InstructionsEditor.tsx b/js/app/packages/block-md/component/InstructionsEditor.tsx index 5e0066ad5f..75eff6387e 100644 --- a/js/app/packages/block-md/component/InstructionsEditor.tsx +++ b/js/app/packages/block-md/component/InstructionsEditor.tsx @@ -53,7 +53,6 @@ import { type PeerIdValidator, peerIdPlugin, } from '@lexical-core'; -import type { MarkdownRewriteOutput } from '@service-cognition/generated/tools/types'; import { debounce } from '@solid-primitives/scheduled'; import { createMethodRegistration } from 'core/orchestrator'; import type { EditorState } from 'lexical'; @@ -67,6 +66,7 @@ import { Show, } from 'solid-js'; import { blockDataSignal, mdStore } from '../signal/markdownBlockData'; +import type { MarkdownRewriteOutput } from '../signal/rewriteSignal'; import { useBlockSave, useSaveMarkdownDocument } from '../signal/save'; import { MarkdownCollabProvider } from './MarkdownCollabProvider'; diff --git a/js/app/packages/block-md/component/MarkdownEditor.tsx b/js/app/packages/block-md/component/MarkdownEditor.tsx index 9a628d88b8..ce3e841831 100644 --- a/js/app/packages/block-md/component/MarkdownEditor.tsx +++ b/js/app/packages/block-md/component/MarkdownEditor.tsx @@ -139,7 +139,6 @@ import { type PeerIdValidator, peerIdPlugin, } from '@lexical-core'; -import type { MarkdownRewriteOutput } from '@service-cognition/generated/tools/types'; import { waitBulkUploadStatus } from '@service-connection/bulkUpload'; import { fileExtension } from '@service-storage/util/filename'; import { createCallback } from '@solid-primitives/rootless'; @@ -159,7 +158,6 @@ import { $isElementNode, type EditorState, } from 'lexical'; - import { type Accessor, createEffect, @@ -179,6 +177,7 @@ import { isGeneratingSignal, } from '../signal/generateSignal'; import { blockDataSignal, mdStore } from '../signal/markdownBlockData'; +import type { MarkdownRewriteOutput } from '../signal/rewriteSignal'; import { useBlockSave, useSaveMarkdownDocument } from '../signal/save'; import { MarkdownCollabProvider } from './MarkdownCollabProvider'; import { MarkdownPopup } from './MarkdownPopup'; diff --git a/js/app/packages/block-md/definition.ts b/js/app/packages/block-md/definition.ts index 7a627ba7a8..2b3a73af36 100644 --- a/js/app/packages/block-md/definition.ts +++ b/js/app/packages/block-md/definition.ts @@ -10,7 +10,6 @@ import { serializedStateFromBlob } from '@core/component/LexicalMarkdown/collabo import { ENABLE_MARKDOWN_LIVE_COLLABORATION } from '@core/constant/featureFlags'; import { isErr, ok } from '@core/util/maybeResult'; import { type SchemaType, schema } from '@loro-mirror/packages/core/src'; -import type { MarkdownRewriteOutput } from '@service-cognition/generated/tools/types'; import { storageServiceClient } from '@service-storage/client'; import { fetchBinary } from '@service-storage/util/fetchBinary'; import { makeFileFromBlob } from '@service-storage/util/makeFileFromBlob'; @@ -19,6 +18,7 @@ import { createSyncServiceSource } from '@service-sync/source'; import { untrack } from 'solid-js'; import { createStore } from 'solid-js/store'; import MarkdownBlock from './component/Block'; +import type { MarkdownRewriteOutput } from './signal/rewriteSignal'; const nodeSchema = schema.LoroMap({ $: schema.LoroMap({} as any, { diff --git a/js/app/packages/block-md/signal/rewriteSignal.ts b/js/app/packages/block-md/signal/rewriteSignal.ts index 9d4465d825..631cb10133 100644 --- a/js/app/packages/block-md/signal/rewriteSignal.ts +++ b/js/app/packages/block-md/signal/rewriteSignal.ts @@ -1,5 +1,10 @@ import { createBlockSignal } from '@core/block'; -import type { MarkdownRewriteOutput } from '@service-cognition/generated/tools/types'; +import type { NamedTool } from '@service-cognition/generated/tools/tool'; + +export type MarkdownRewriteOutput = NamedTool< + 'MarkdownRewrite', + 'response' +>['data']; export const rewriteSignal = createBlockSignal(false); export const isRewritingSignal = createBlockSignal(false); diff --git a/js/app/packages/core/component/AI/component/debug/Component.tsx b/js/app/packages/core/component/AI/component/debug/Component.tsx index 22623b5d78..d7c2862163 100644 --- a/js/app/packages/core/component/AI/component/debug/Component.tsx +++ b/js/app/packages/core/component/AI/component/debug/Component.tsx @@ -276,7 +276,7 @@ function ToolCallResponseRender() { { type: 'toolCall', tool: { - name: 'WebSearch', + name: 'web_search', data: { query: 'most important headlines today', }, @@ -285,10 +285,16 @@ function ToolCallResponseRender() { { type: 'toolResponse', tool: { - name: 'WebSearch', + name: 'web_search', data: { - results: [{ name: 'news.com', url: 'www.news.com' }], - content: 'I read the results and there is news!!!', + content: [ + { + type: 'web_search_result', + title: 'news.com', + url: 'www.news.com', + }, + ], + tool_use_id: 'I read the results and there is news!!!', }, }, }, diff --git a/js/app/packages/core/component/AI/component/debug/Tool.tsx b/js/app/packages/core/component/AI/component/debug/Tool.tsx index d6ec90564a..ddf52f8a32 100644 --- a/js/app/packages/core/component/AI/component/debug/Tool.tsx +++ b/js/app/packages/core/component/AI/component/debug/Tool.tsx @@ -25,7 +25,7 @@ function ToolCall() { { type: 'toolCall', tool: { - name: 'WebSearch', + name: 'web_search', data: { query: 'Weather in nyc now', }, @@ -48,7 +48,7 @@ function ToolResponse() { { type: 'toolCall', tool: { - name: 'WebSearch', + name: 'web_search', data: { query: 'Weather in nyc now', }, @@ -58,15 +58,16 @@ function ToolResponse() { type: 'toolResponse', tool: { data: { - content: 'it is sunny', - results: [ + tool_use_id: 'hehexd', + content: [ { - name: 'weather.com', + title: 'its sunny', + type: 'web_search_result', url: 'https://weather.com', }, ], }, - name: 'WebSearch', + name: 'web_search', }, }, { @@ -90,7 +91,7 @@ function ToolResponsStreamEnd() { { type: 'toolCall', tool: { - name: 'WebSearch', + name: 'web_search', data: { query: 'Weather in nyc now', }, @@ -100,15 +101,16 @@ function ToolResponsStreamEnd() { type: 'toolResponse', tool: { data: { - content: 'it is sunny', - results: [ + tool_use_id: 'hehexd', + content: [ { - name: 'weather.com', + title: 'its sunny', + type: 'web_search_result', url: 'https://weather.com', }, ], }, - name: 'WebSearch', + name: 'web_search', }, }, { diff --git a/js/app/packages/core/component/AI/component/tool/ListDocuments.tsx b/js/app/packages/core/component/AI/component/tool/ListDocuments.tsx index f1a5fa3d2e..507cf647e6 100644 --- a/js/app/packages/core/component/AI/component/tool/ListDocuments.tsx +++ b/js/app/packages/core/component/AI/component/tool/ListDocuments.tsx @@ -4,7 +4,7 @@ import { fileTypeToBlockName } from '@core/constant/allBlocks'; import ChevronDown from '@icon/regular/caret-down.svg?component-solid'; import ChevronUp from '@icon/regular/caret-up.svg?component-solid'; import List from '@phosphor-icons/core/regular/list.svg'; -import type { ListDocumentsResult } from '@service-cognition/toolTypes'; +import type { NamedTool } from '@service-cognition/generated/tools/tool'; import type { FileType } from '@service-storage/generated/schemas/fileType'; import { useSplitLayout } from 'app/component/split-layout/layout'; import { createMemo, createSignal, Show } from 'solid-js'; @@ -12,6 +12,11 @@ import { VList } from 'virtua/solid'; import { BaseTool } from './BaseTool'; import { createToolRenderer } from './ToolRenderer'; +type ListDocumentsResult = NamedTool< + 'ListDocuments', + 'response' +>['data']['results'][number]; + const ListDocumentsToolResponse = (props: { results: ListDocumentsResult[]; }) => { diff --git a/js/app/packages/core/component/AI/component/tool/ListEmails.tsx b/js/app/packages/core/component/AI/component/tool/ListEmails.tsx index ccfb75e3ba..bc980ca62d 100644 --- a/js/app/packages/core/component/AI/component/tool/ListEmails.tsx +++ b/js/app/packages/core/component/AI/component/tool/ListEmails.tsx @@ -3,13 +3,18 @@ import { TruncatedText } from '@core/component/FileList/TruncatedText'; import ChevronDown from '@icon/regular/caret-down.svg?component-solid'; import ChevronUp from '@icon/regular/caret-up.svg?component-solid'; import List from '@phosphor-icons/core/regular/list.svg'; -import type { ListEmailsResult } from '@service-cognition/toolTypes'; +import type { NamedTool } from '@service-cognition/generated/tools/tool'; import { useSplitLayout } from 'app/component/split-layout/layout'; import { createMemo, createSignal, Show } from 'solid-js'; import { VList } from 'virtua/solid'; import { BaseTool } from './BaseTool'; import { createToolRenderer } from './ToolRenderer'; +type ListEmailsResult = NamedTool< + 'ListEmails', + 'response' +>['data']['items'][number]; + const ListEmailsToolResponse = (props: { results: ListEmailsResult[] }) => { const [isExpanded, setIsExpanded] = createSignal(false); diff --git a/js/app/packages/core/component/AI/component/tool/UnifiedSearch.tsx b/js/app/packages/core/component/AI/component/tool/UnifiedSearch.tsx index ba81d521ff..76ff049614 100644 --- a/js/app/packages/core/component/AI/component/tool/UnifiedSearch.tsx +++ b/js/app/packages/core/component/AI/component/tool/UnifiedSearch.tsx @@ -4,7 +4,7 @@ import { fileTypeToBlockName } from '@core/constant/allBlocks'; import ChevronDown from '@icon/regular/caret-down.svg?component-solid'; import ChevronUp from '@icon/regular/caret-up.svg?component-solid'; import MagnifyingGlass from '@phosphor-icons/core/regular/magnifying-glass.svg'; -import type { UnifiedSearchResult } from '@service-cognition/toolTypes'; +import type { NamedTool } from '@service-cognition/generated/tools/tool'; import type { FileType } from '@service-storage/generated/schemas/fileType'; import { useSplitLayout } from 'app/component/split-layout/layout'; import { createMemo, createSignal, Show } from 'solid-js'; @@ -12,6 +12,11 @@ import { VList } from 'virtua/solid'; import { BaseTool } from './BaseTool'; import { createToolRenderer } from './ToolRenderer'; +type UnifiedSearchResult = NamedTool< + 'UnifiedSearch', + 'response' +>['data']['response']['results'][number]; + const UnifiedSearchToolResponse = (props: { results: UnifiedSearchResult[]; }) => { diff --git a/js/app/packages/core/component/AI/component/tool/WebSearch.tsx b/js/app/packages/core/component/AI/component/tool/WebSearch.tsx index 8b84830dfd..1e9e2c35ed 100644 --- a/js/app/packages/core/component/AI/component/tool/WebSearch.tsx +++ b/js/app/packages/core/component/AI/component/tool/WebSearch.tsx @@ -4,7 +4,7 @@ import { BaseTool } from './BaseTool'; import { createToolRenderer } from './ToolRenderer'; const handler = createToolRenderer({ - name: 'WebSearch', + name: 'web_search', renderCall: (ctx) => ( ({ - title: result.name, + links={ctx.tool.data.content.map((result) => ({ + title: result.title, url: result.url, }))} /> diff --git a/js/app/packages/core/component/AI/component/tool/handler.tsx b/js/app/packages/core/component/AI/component/tool/handler.tsx index f4bd3252f5..d2647e3c35 100644 --- a/js/app/packages/core/component/AI/component/tool/handler.tsx +++ b/js/app/packages/core/component/AI/component/tool/handler.tsx @@ -24,7 +24,7 @@ const [renderStore, setRenderStore] = createStore< const toolHandlers: ToolHandlerMap = { UnifiedSearch: unifiedSearchHandler, - WebSearch: webSearchHandler, + web_search: webSearchHandler, MarkdownRewrite: rewriteHandler, Read: readHandler, ListDocuments: listDocumentsHandler, diff --git a/js/app/packages/core/component/LexicalMarkdown/plugins/diff/diffPlugin.ts b/js/app/packages/core/component/LexicalMarkdown/plugins/diff/diffPlugin.ts index efdbdae77f..eff4aa2af6 100644 --- a/js/app/packages/core/component/LexicalMarkdown/plugins/diff/diffPlugin.ts +++ b/js/app/packages/core/component/LexicalMarkdown/plugins/diff/diffPlugin.ts @@ -9,7 +9,7 @@ import { $getNodeById, $isDiffNode, } from '@lexical-core'; -import type { MarkdownRewriteOutput } from '@service-cognition/generated/tools/types'; +import type { NamedTool } from '@service-cognition/generated/tools/tool'; import { useUserId } from '@service-gql/client'; import type { LexicalEditor } from 'lexical'; import { @@ -20,7 +20,7 @@ import { import { createEffect } from 'solid-js'; import { mapRegisterDelete } from '../shared/utils'; -type Diff = MarkdownRewriteOutput['diffs'][number]; +type Diff = NamedTool<'MarkdownRewrite', 'response'>['data']['diffs'][number]; export type DiffPluginArgs = { revisionsSignal: ReturnType>; diff --git a/js/app/packages/service-clients/service-cognition/generated/tools/schemas.ts b/js/app/packages/service-clients/service-cognition/generated/tools/schemas.ts index c3c443c0f9..89e470593f 100644 --- a/js/app/packages/service-clients/service-cognition/generated/tools/schemas.ts +++ b/js/app/packages/service-clients/service-cognition/generated/tools/schemas.ts @@ -4,28 +4,28 @@ */ import { z } from 'zod'; -export const ListDocumentsInputSchema = z.object({ "exhaustiveSearch": z.boolean().default(false), "fileTypes": z.union([z.array(z.string()), z.null()]), "minAccessLevel": z.union([z.literal("view"), z.literal("comment"), z.literal("edit"), z.literal("owner"), z.literal(null)]), "pageOffset": z.number().int().default(0), "pageSize": z.number().int().default(50) }).strict() +export const ListDocuments = z.object({ "exhaustiveSearch": z.boolean().default(false), "fileTypes": z.union([z.array(z.string()), z.null()]), "minAccessLevel": z.union([z.literal("view"), z.literal("comment"), z.literal("edit"), z.literal("owner"), z.literal(null)]), "pageOffset": z.number().int().default(0), "pageSize": z.number().int().default(50) }).strict() -export const ListDocumentsOutputSchema = z.object({ "results": z.array(z.object({ "access_level": z.enum(["view","comment","edit","owner"]), "created_at": z.string().datetime({ offset: true }), "deleted_at": z.union([z.string().datetime({ offset: true }), z.null()]).optional(), "document_id": z.string(), "document_name": z.string(), "file_type": z.union([z.string(), z.null()]).optional(), "owner": z.string(), "project_id": z.union([z.string(), z.null()]).optional(), "updated_at": z.string().datetime({ offset: true }) })), "resultsReturned": z.number().int().gte(0), "totalResults": z.number().int().gte(0) }) +export const ListDocumentsResponse = z.object({ "results": z.array(z.object({ "access_level": z.enum(["view","comment","edit","owner"]), "created_at": z.string().datetime({ offset: true }), "deleted_at": z.union([z.string().datetime({ offset: true }), z.null()]).optional(), "document_id": z.string(), "document_name": z.string(), "file_type": z.union([z.string(), z.null()]).optional(), "owner": z.string(), "project_id": z.union([z.string(), z.null()]).optional(), "updated_at": z.string().datetime({ offset: true }) })), "resultsReturned": z.number().int().gte(0), "totalResults": z.number().int().gte(0) }) -export const ListEmailsInputSchema = z.object({ "cursor": z.union([z.string(), z.null()]).default(null), "limit": z.number().int().default(20), "sortMethod": z.enum(["viewed_at","updated_at","created_at","viewed_updated"]).default("viewed_at"), "view": z.object({ "standardLabel": z.union([z.literal("Inbox"), z.literal("Sent"), z.literal("Drafts"), z.literal("Starred"), z.literal("All"), z.literal("Important"), z.literal("Other"), z.literal(null)]), "userLabel": z.union([z.string(), z.null()]) }).strict().default({"standardLabel":null,"userLabel":null}) }).strict() +export const ListEmails = z.object({ "cursor": z.union([z.string(), z.null()]).default(null), "limit": z.number().int().default(20), "sortMethod": z.enum(["viewed_at","updated_at","created_at","viewed_updated"]).default("viewed_at"), "view": z.object({ "standardLabel": z.union([z.literal("Inbox"), z.literal("Sent"), z.literal("Drafts"), z.literal("Starred"), z.literal("All"), z.literal("Important"), z.literal("Other"), z.literal(null)]), "userLabel": z.union([z.string(), z.null()]) }).strict().default({"standardLabel":null,"userLabel":null}) }).strict() -export const ListEmailsOutputSchema = z.object({ "items": z.array(z.object({ "attachments": z.array(z.object({ "contentId": z.union([z.string(), z.null()]).optional(), "createdAt": z.number().int(), "filename": z.union([z.string(), z.null()]).optional(), "id": z.string().uuid(), "messageId": z.string().uuid(), "mimeType": z.union([z.string(), z.null()]).optional(), "providerAttachmentId": z.union([z.string(), z.null()]).optional(), "sizeBytes": z.union([z.number().int(), z.null()]).optional() })), "contacts": z.array(z.object({ "emailAddress": z.union([z.string(), z.null()]).optional(), "id": z.string().uuid(), "linkId": z.string().uuid(), "name": z.union([z.string(), z.null()]).optional(), "sfsPhotoUrl": z.union([z.string(), z.null()]).optional() })), "createdAt": z.number().int(), "frecencyScore": z.union([z.number(), z.null()]).optional(), "id": z.string().uuid(), "inboxVisible": z.boolean(), "isDraft": z.boolean(), "isImportant": z.boolean(), "isRead": z.boolean(), "labels": z.array(z.object({ "createdAt": z.string().datetime({ offset: true }), "id": z.string().uuid(), "labelListVisibility": z.enum(["LabelShow","LabelShowIfUnread","LabelHide"]), "linkId": z.string().uuid(), "messageListVisibility": z.enum(["Show","Hide"]), "name": z.string(), "providerLabelId": z.string(), "type": z.enum(["System","User"]) })), "macroAttachments": z.array(z.object({ "dbId": z.string().uuid(), "itemId": z.string().uuid(), "itemType": z.string(), "messageId": z.string().uuid() })), "metadata": z.object({ "calendar_invite": z.boolean(), "generic_sender": z.boolean(), "known_sender": z.boolean(), "tabular": z.boolean() }), "name": z.union([z.string(), z.null()]).optional(), "ownerId": z.string(), "providerId": z.union([z.string(), z.null()]).optional(), "senderEmail": z.union([z.string(), z.null()]).optional(), "senderName": z.union([z.string(), z.null()]).optional(), "senderPhotoUrl": z.union([z.string(), z.null()]).optional(), "snippet": z.union([z.string(), z.null()]).optional(), "sortTs": z.number().int(), "updatedAt": z.number().int(), "viewedAt": z.union([z.number().int(), z.null()]).optional() })), "next_cursor": z.union([z.string(), z.null()]).optional() }) +export const ApiPaginatedThreadCursor = z.object({ "items": z.array(z.object({ "attachments": z.array(z.object({ "contentId": z.union([z.string(), z.null()]).optional(), "createdAt": z.number().int(), "filename": z.union([z.string(), z.null()]).optional(), "id": z.string().uuid(), "messageId": z.string().uuid(), "mimeType": z.union([z.string(), z.null()]).optional(), "providerAttachmentId": z.union([z.string(), z.null()]).optional(), "sizeBytes": z.union([z.number().int(), z.null()]).optional() })), "contacts": z.array(z.object({ "emailAddress": z.union([z.string(), z.null()]).optional(), "id": z.string().uuid(), "linkId": z.string().uuid(), "name": z.union([z.string(), z.null()]).optional(), "sfsPhotoUrl": z.union([z.string(), z.null()]).optional() })), "createdAt": z.number().int(), "frecencyScore": z.union([z.number(), z.null()]).optional(), "id": z.string().uuid(), "inboxVisible": z.boolean(), "isDraft": z.boolean(), "isImportant": z.boolean(), "isRead": z.boolean(), "labels": z.array(z.object({ "createdAt": z.string().datetime({ offset: true }), "id": z.string().uuid(), "labelListVisibility": z.enum(["LabelShow","LabelShowIfUnread","LabelHide"]), "linkId": z.string().uuid(), "messageListVisibility": z.enum(["Show","Hide"]), "name": z.string(), "providerLabelId": z.string(), "type": z.enum(["System","User"]) })), "macroAttachments": z.array(z.object({ "dbId": z.string().uuid(), "itemId": z.string().uuid(), "itemType": z.string(), "messageId": z.string().uuid() })), "metadata": z.object({ "calendar_invite": z.boolean(), "generic_sender": z.boolean(), "known_sender": z.boolean(), "tabular": z.boolean() }), "name": z.union([z.string(), z.null()]).optional(), "ownerId": z.string(), "providerId": z.union([z.string(), z.null()]).optional(), "senderEmail": z.union([z.string(), z.null()]).optional(), "senderName": z.union([z.string(), z.null()]).optional(), "senderPhotoUrl": z.union([z.string(), z.null()]).optional(), "snippet": z.union([z.string(), z.null()]).optional(), "sortTs": z.number().int(), "updatedAt": z.number().int(), "viewedAt": z.union([z.number().int(), z.null()]).optional() })), "next_cursor": z.union([z.string(), z.null()]).optional() }) -export const MarkdownRewriteInputSchema = z.object({ "instructions": z.string(), "markdown_file_id": z.string() }).strict() +export const MarkdownRewrite = z.object({ "instructions": z.string(), "markdown_file_id": z.string() }).strict() -export const MarkdownRewriteOutputSchema = z.object({ "diffs": z.array(z.object({ "markdown_text": z.string(), "node_key": z.string(), "operation": z.string() }).strict()) }).strict() +export const AIDiffResponse = z.object({ "diffs": z.array(z.object({ "markdown_text": z.string(), "node_key": z.string(), "operation": z.string() }).strict()) }).strict() -export const ReadInputSchema = z.object({ "contentType": z.enum(["document","channel","channel-message","chat-thread","chat-message","email-thread","email-message","project"]), "ids": z.array(z.string()), "messagesSince": z.union([z.string().datetime({ offset: true }), z.null()]) }).strict() +export const Read = z.object({ "contentType": z.enum(["document","channel","channel-message","chat-thread","chat-message","email-thread","email-message","project"]), "ids": z.array(z.string()), "messagesSince": z.union([z.string().datetime({ offset: true }), z.null()]) }).strict() -export const ReadOutputSchema = z.object({ "content": z.any().superRefine((x, ctx) => { +export const ReadResponse = z.object({ "content": z.any().superRefine((x, ctx) => { const schemas = [z.object({ "documents": z.array(z.object({ "content": z.string(), "documentId": z.string(), "metadata": z.object({ "deleted": z.boolean(), "documentName": z.string(), "fileType": z.union([z.string(), z.null()]).optional(), "owner": z.string(), "projectId": z.union([z.string(), z.null()]).optional() }) })), "type": z.literal("documents") }), z.object({ "channel_id": z.string(), "channel_name": z.union([z.string(), z.null()]).optional(), "transcript": z.string(), "type": z.literal("channel") }), z.object({ "conversation": z.array(z.object({ "chat_id": z.string(), "messages": z.array(z.object({ "attachment_summaries": z.array(z.any().superRefine((x, ctx) => { const schemas = [z.object({ "Summary": z.object({ "created_at": z.union([z.string().datetime({ offset: true }), z.null()]).optional(), "document_id": z.string(), "id": z.union([z.string(), z.null()]).optional(), "summary": z.string(), "version_id": z.string() }) }).strict(), z.object({ "NoSummary": z.object({ "document_id": z.string() }) }).strict()]; const errors = schemas.reduce( @@ -64,11 +64,11 @@ export const ReadOutputSchema = z.object({ "content": z.any().superRefine((x, ct }) }) -export const UnifiedSearchInputSchema = z.object({ "exhaustiveSearch": z.boolean().default(true), "pageOffset": z.number().int().default(0), "pageSize": z.number().int().default(10), "request": z.object({ "filters": z.union([z.object({ "channel": z.union([z.object({ "channel_ids": z.array(z.string()), "mentions": z.array(z.string()), "org_id": z.union([z.number().int(), z.null()]), "sender_ids": z.array(z.string()), "thread_ids": z.array(z.string()) }).strict(), z.null()]), "chat": z.union([z.object({ "chat_ids": z.array(z.string()), "owners": z.array(z.string()), "project_ids": z.array(z.string()), "role": z.array(z.string()) }).strict(), z.null()]), "document": z.union([z.object({ "document_ids": z.array(z.string()), "file_types": z.array(z.string()), "owners": z.array(z.string()), "project_ids": z.array(z.string()) }).strict(), z.null()]), "email": z.union([z.object({ "bcc": z.array(z.string()), "cc": z.array(z.string()), "recipients": z.array(z.string()), "senders": z.array(z.string()) }).strict(), z.null()]), "project": z.union([z.object({ "owners": z.array(z.string()), "project_ids": z.array(z.string()) }).strict(), z.null()]) }).strict(), z.null()]), "include": z.array(z.enum(["documents","chats","emails","channels","projects"])).default([]), "match_type": z.enum(["exact","partial","regexp"]), "search_on": z.enum(["name","content","name_content"]).default("content"), "terms": z.union([z.array(z.string()), z.null()]) }).strict() }).strict() +export const UnifiedSearch = z.object({ "exhaustiveSearch": z.boolean().default(true), "pageOffset": z.number().int().default(0), "pageSize": z.number().int().default(10), "request": z.object({ "filters": z.union([z.object({ "channel": z.union([z.object({ "channel_ids": z.array(z.string()), "mentions": z.array(z.string()), "org_id": z.union([z.number().int(), z.null()]), "sender_ids": z.array(z.string()), "thread_ids": z.array(z.string()) }).strict(), z.null()]), "chat": z.union([z.object({ "chat_ids": z.array(z.string()), "owners": z.array(z.string()), "project_ids": z.array(z.string()), "role": z.array(z.string()) }).strict(), z.null()]), "document": z.union([z.object({ "document_ids": z.array(z.string()), "file_types": z.array(z.string()), "owners": z.array(z.string()), "project_ids": z.array(z.string()) }).strict(), z.null()]), "email": z.union([z.object({ "bcc": z.array(z.string()), "cc": z.array(z.string()), "recipients": z.array(z.string()), "senders": z.array(z.string()) }).strict(), z.null()]), "project": z.union([z.object({ "owners": z.array(z.string()), "project_ids": z.array(z.string()) }).strict(), z.null()]) }).strict(), z.null()]), "include": z.array(z.enum(["documents","chats","emails","channels","projects"])).default([]), "match_type": z.enum(["exact","partial","regexp"]), "search_on": z.enum(["name","content","name_content"]).default("content"), "terms": z.union([z.array(z.string()), z.null()]) }).strict() }).strict() -export const UnifiedSearchOutputSchema = z.object({ "response": z.object({ "results": z.array(z.any().superRefine((x, ctx) => { - const schemas = [z.object({ "document_id": z.string(), "document_name": z.string(), "file_type": z.string(), "highlight": z.object({ "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional() }), "node_id": z.string(), "owner_id": z.string(), "raw_content": z.union([z.string(), z.null()]).optional(), "type": z.literal("document"), "updated_at": z.string().datetime({ offset: true }) }), z.object({ "chat_id": z.string(), "chat_message_id": z.string(), "highlight": z.object({ "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional() }), "role": z.string(), "title": z.string(), "type": z.literal("chat"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() }), z.object({ "bcc": z.array(z.string()), "cc": z.array(z.string()), "highlight": z.object({ "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional() }), "labels": z.array(z.string()), "link_id": z.string(), "message_id": z.string(), "recipients": z.array(z.string()), "sender": z.string(), "sent_at": z.union([z.string().datetime({ offset: true }), z.null()]).optional(), "subject": z.union([z.string(), z.null()]).optional(), "thread_id": z.string(), "type": z.literal("email"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() }), z.object({ "channel_id": z.string(), "channel_type": z.string(), "created_at": z.string().datetime({ offset: true }), "highlight": z.object({ "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional() }), "mentions": z.array(z.string()), "message_id": z.string(), "org_id": z.union([z.number().int(), z.null()]).optional(), "sender_id": z.string(), "thread_id": z.union([z.string(), z.null()]).optional(), "type": z.literal("channel"), "updated_at": z.string().datetime({ offset: true }) }), z.object({ "created_at": z.string().datetime({ offset: true }), "highlight": z.object({ "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional() }), "project_id": z.string(), "project_name": z.string(), "type": z.literal("project"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() })]; +export const UnifiedSearchResponseOutput = z.object({ "response": z.object({ "results": z.array(z.any().superRefine((x, ctx) => { + const schemas = [z.object({ "document_id": z.string(), "document_name": z.string(), "file_type": z.string(), "highlight": z.object({ "bcc": z.array(z.string()).optional(), "cc": z.array(z.string()).optional(), "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional(), "recipients": z.array(z.string()).optional(), "sender": z.union([z.string(), z.null()]).optional(), "user_id": z.union([z.string(), z.null()]).optional() }), "node_id": z.string(), "owner_id": z.string(), "raw_content": z.union([z.string(), z.null()]).optional(), "type": z.literal("document"), "updated_at": z.string().datetime({ offset: true }) }), z.object({ "chat_id": z.string(), "chat_message_id": z.string(), "highlight": z.object({ "bcc": z.array(z.string()).optional(), "cc": z.array(z.string()).optional(), "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional(), "recipients": z.array(z.string()).optional(), "sender": z.union([z.string(), z.null()]).optional(), "user_id": z.union([z.string(), z.null()]).optional() }), "role": z.string(), "title": z.string(), "type": z.literal("chat"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() }), z.object({ "bcc": z.array(z.string()), "cc": z.array(z.string()), "highlight": z.object({ "bcc": z.array(z.string()).optional(), "cc": z.array(z.string()).optional(), "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional(), "recipients": z.array(z.string()).optional(), "sender": z.union([z.string(), z.null()]).optional(), "user_id": z.union([z.string(), z.null()]).optional() }), "labels": z.array(z.string()), "link_id": z.string(), "message_id": z.string(), "recipients": z.array(z.string()), "sender": z.string(), "sent_at": z.union([z.string().datetime({ offset: true }), z.null()]).optional(), "subject": z.union([z.string(), z.null()]).optional(), "thread_id": z.string(), "type": z.literal("email"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() }), z.object({ "channel_id": z.string(), "channel_type": z.string(), "created_at": z.string().datetime({ offset: true }), "highlight": z.object({ "bcc": z.array(z.string()).optional(), "cc": z.array(z.string()).optional(), "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional(), "recipients": z.array(z.string()).optional(), "sender": z.union([z.string(), z.null()]).optional(), "user_id": z.union([z.string(), z.null()]).optional() }), "mentions": z.array(z.string()), "message_id": z.string(), "org_id": z.union([z.number().int(), z.null()]).optional(), "sender_id": z.string(), "thread_id": z.union([z.string(), z.null()]).optional(), "type": z.literal("channel"), "updated_at": z.string().datetime({ offset: true }) }), z.object({ "created_at": z.string().datetime({ offset: true }), "highlight": z.object({ "bcc": z.array(z.string()).optional(), "cc": z.array(z.string()).optional(), "content": z.array(z.string()).optional(), "name": z.union([z.string(), z.null()]).optional(), "recipients": z.array(z.string()).optional(), "sender": z.union([z.string(), z.null()]).optional(), "user_id": z.union([z.string(), z.null()]).optional() }), "project_id": z.string(), "project_name": z.string(), "type": z.literal("project"), "updated_at": z.string().datetime({ offset: true }), "user_id": z.string() })]; const errors = schemas.reduce( (errors, schema) => ((result) => @@ -88,8 +88,8 @@ export const UnifiedSearchOutputSchema = z.object({ "response": z.object({ "resu })), "resultsReturned": z.number().int().gte(0), "totalResults": z.number().int().gte(0) }), "responseSchema": z.any() }) -export const WebSearchInputSchema = z.object({ "query": z.string() }).strict() +export const WebSearchToolCall = z.object({ "query": z.string() }) -export const WebSearchOutputSchema = z.object({ "content": z.string(), "results": z.array(z.object({ "name": z.string(), "url": z.string() })) }) +export const WebSearchResponse = z.object({ "content": z.array(z.object({ "title": z.string(), "type": z.literal("web_search_result"), "url": z.string() })), "tool_use_id": z.string() }) diff --git a/js/app/packages/service-clients/service-cognition/generated/tools/tool.ts b/js/app/packages/service-clients/service-cognition/generated/tools/tool.ts index 51529a1a0e..cbb7253a53 100644 --- a/js/app/packages/service-clients/service-cognition/generated/tools/tool.ts +++ b/js/app/packages/service-clients/service-cognition/generated/tools/tool.ts @@ -9,21 +9,21 @@ import * as schemas from './schemas'; import type * as types from './types'; type ToolParserMap = { -ListDocuments: { call: types.ListDocumentsInput, response: types.ListDocumentsOutput } -ListEmails: { call: types.ListEmailsInput, response: types.ListEmailsOutput } -MarkdownRewrite: { call: types.MarkdownRewriteInput, response: types.MarkdownRewriteOutput } -Read: { call: types.ReadInput, response: types.ReadOutput } -UnifiedSearch: { call: types.UnifiedSearchInput, response: types.UnifiedSearchOutput } -WebSearch: { call: types.WebSearchInput, response: types.WebSearchOutput } +ListDocuments: { call: types.ListDocuments, response: types.ListDocumentsResponse } +ListEmails: { call: types.ListEmails, response: types.ApiPaginatedThreadCursor } +MarkdownRewrite: { call: types.MarkdownRewrite, response: types.AIDiffResponse } +Read: { call: types.Read, response: types.ReadResponse } +UnifiedSearch: { call: types.UnifiedSearch, response: types.UnifiedSearchResponseOutput } +web_search: { call: types.WebSearchToolCall, response: types.WebSearchResponse } }; const toolParserMap = { -ListDocuments: { call: schemas.ListDocumentsInputSchema, response: schemas.ListDocumentsOutputSchema }, -ListEmails: { call: schemas.ListEmailsInputSchema, response: schemas.ListEmailsOutputSchema }, -MarkdownRewrite: { call: schemas.MarkdownRewriteInputSchema, response: schemas.MarkdownRewriteOutputSchema }, -Read: { call: schemas.ReadInputSchema, response: schemas.ReadOutputSchema }, -UnifiedSearch: { call: schemas.UnifiedSearchInputSchema, response: schemas.UnifiedSearchOutputSchema }, -WebSearch: { call: schemas.WebSearchInputSchema, response: schemas.WebSearchOutputSchema } +ListDocuments: { call: schemas.ListDocuments, response: schemas.ListDocumentsResponse }, +ListEmails: { call: schemas.ListEmails, response: schemas.ApiPaginatedThreadCursor }, +MarkdownRewrite: { call: schemas.MarkdownRewrite, response: schemas.AIDiffResponse }, +Read: { call: schemas.Read, response: schemas.ReadResponse }, +UnifiedSearch: { call: schemas.UnifiedSearch, response: schemas.UnifiedSearchResponseOutput }, +web_search: { call: schemas.WebSearchToolCall, response: schemas.WebSearchResponse } }; export type ToolName = keyof ToolParserMap; @@ -35,12 +35,12 @@ type NamedRawTool = { }; type ToolDataMap = { -ListDocuments: { call: types.ListDocumentsInput, response: types.ListDocumentsOutput }; -ListEmails: { call: types.ListEmailsInput, response: types.ListEmailsOutput }; -MarkdownRewrite: { call: types.MarkdownRewriteInput, response: types.MarkdownRewriteOutput }; -Read: { call: types.ReadInput, response: types.ReadOutput }; -UnifiedSearch: { call: types.UnifiedSearchInput, response: types.UnifiedSearchOutput }; -WebSearch: { call: types.WebSearchInput, response: types.WebSearchOutput }; +ListDocuments: { call: types.ListDocuments, response: types.ListDocumentsResponse }; +ListEmails: { call: types.ListEmails, response: types.ApiPaginatedThreadCursor }; +MarkdownRewrite: { call: types.MarkdownRewrite, response: types.AIDiffResponse }; +Read: { call: types.Read, response: types.ReadResponse }; +UnifiedSearch: { call: types.UnifiedSearch, response: types.UnifiedSearchResponseOutput }; +web_search: { call: types.WebSearchToolCall, response: types.WebSearchResponse }; }; export type NamedTool< diff --git a/js/app/packages/service-clients/service-cognition/generated/tools/types.ts b/js/app/packages/service-clients/service-cognition/generated/tools/types.ts index 8a894c406c..772534b544 100644 --- a/js/app/packages/service-clients/service-cognition/generated/tools/types.ts +++ b/js/app/packages/service-clients/service-cognition/generated/tools/types.ts @@ -12,7 +12,7 @@ /** * List documents the user has access to with optional filtering and pagination. Only applies to documents, not emails, AI conversations, chat/slack threads, projects aka folders. This tool returns document metadata including access levels and supports filtering by file type, minimum access level, and pagination. Use this tool to discover and browse documents before using the Read tool to access their content. Prefer using the search tool to search on a specific matching string within the content or the name of the entity. */ -export interface ListDocumentsInput { +export interface ListDocuments { /** * Exhaustive search to get all results. Defaults to false. Set to true when you need all matching documents, ignoring pagination limits. */ @@ -43,7 +43,7 @@ export interface ListDocumentsInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface ListDocumentsOutput { +export interface ListDocumentsResponse { /** * The list results (truncated to `results_returned` limit if applicable) */ @@ -106,7 +106,7 @@ export interface ListDocumentsOutput { /** * List the emails the user has access to. Use this tool to discover and browse emails before using the Read tool to access their content. Prefer using the search tool to search on a specific matching string within the content or the name of the entity. */ -export interface ListEmailsInput { +export interface ListEmails { /** * A preview response will tell you what the next cursor id is. * If expected emails are not in the current response and it gives you a cursor id, use this field to list the next page of emails. If no cursor id is provided you've reached the end @@ -143,7 +143,7 @@ export interface ListEmailsInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface ListEmailsOutput { +export interface ApiPaginatedThreadCursor { items: { attachments: { contentId?: string | null; @@ -229,7 +229,7 @@ export interface ListEmailsOutput { * Instruct an agent to edit a markdown file identified by an id. * This tool should be used when the user include a markdown file in context and requests a revision or edit to that file */ -export interface MarkdownRewriteInput { +export interface MarkdownRewrite { /** * Instructions for the revision agent to follow to edit the markdown. These instructions will be provided by the user. */ @@ -248,7 +248,7 @@ export interface MarkdownRewriteInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface MarkdownRewriteOutput { +export interface AIDiffResponse { diffs: { markdown_text: string; node_key: string; @@ -268,7 +268,7 @@ export interface MarkdownRewriteOutput { * Read content by ID(s). Supports reading documents, channels, chats, and emails by their respective IDs. Use this tool when you need to retrieve the full content of a specific item(s). * Channel transcripts only include 300 messages. Use 'messages_since' to see messages in a different time window. */ -export interface ReadInput { +export interface Read { /** * The type of content to read. Choose based on the type of content you want to retrieve. */ @@ -299,7 +299,7 @@ export interface ReadInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface ReadOutput { +export interface ReadResponse { content: | { documents: { @@ -379,7 +379,7 @@ export interface ReadOutput { /** * Universal search across all content types (documents, emails, AI conversations, chat/slack threads/channels, projects aka folders). This tool will return broad metadata from successful results and/or content matches. Use the Read tool next to read the results from those matches. Only ever refer to documents by name or with a document mention. Never state the id of the document in plaintext. User's are presented with the results of this tool as a UI element so there is no need to enumerate the information found from this tool. */ -export interface UnifiedSearchInput { +export interface UnifiedSearch { /** * Exhaustive search across all results matching the query. Defaults to true. Use false when the user only requires a limitied subset of results to answer the question */ @@ -530,7 +530,7 @@ export interface UnifiedSearchInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface UnifiedSearchOutput { +export interface UnifiedSearchResponseOutput { /** * The search results */ @@ -556,6 +556,14 @@ export interface UnifiedSearchOutput { * The highlights on the document */ highlight: { + /** + * The highlight match on the bcc (email only) + */ + bcc?: string[]; + /** + * The highlight match on the cc (email only) + */ + cc?: string[]; /** * The highlight match on the content field */ @@ -564,6 +572,18 @@ export interface UnifiedSearchOutput { * The highlight match on the name field */ name?: string | null; + /** + * The highlight match on the recipients (email only) + */ + recipients?: string[]; + /** + * The highlight match on the sender (email only) + */ + sender?: string | null; + /** + * The highlight match on the user (owner) of the entity + */ + user_id?: string | null; }; /** * The node id @@ -596,6 +616,14 @@ export interface UnifiedSearchOutput { * The highlights on the chat */ highlight: { + /** + * The highlight match on the bcc (email only) + */ + bcc?: string[]; + /** + * The highlight match on the cc (email only) + */ + cc?: string[]; /** * The highlight match on the content field */ @@ -604,6 +632,18 @@ export interface UnifiedSearchOutput { * The highlight match on the name field */ name?: string | null; + /** + * The highlight match on the recipients (email only) + */ + recipients?: string[]; + /** + * The highlight match on the sender (email only) + */ + sender?: string | null; + /** + * The highlight match on the user (owner) of the entity + */ + user_id?: string | null; }; /** * The role @@ -636,6 +676,14 @@ export interface UnifiedSearchOutput { * The highlights on the email */ highlight: { + /** + * The highlight match on the bcc (email only) + */ + bcc?: string[]; + /** + * The highlight match on the cc (email only) + */ + cc?: string[]; /** * The highlight match on the content field */ @@ -644,6 +692,18 @@ export interface UnifiedSearchOutput { * The highlight match on the name field */ name?: string | null; + /** + * The highlight match on the recipients (email only) + */ + recipients?: string[]; + /** + * The highlight match on the sender (email only) + */ + sender?: string | null; + /** + * The highlight match on the user (owner) of the entity + */ + user_id?: string | null; }; /** * The labels @@ -704,6 +764,14 @@ export interface UnifiedSearchOutput { * The highlights on the channel message */ highlight: { + /** + * The highlight match on the bcc (email only) + */ + bcc?: string[]; + /** + * The highlight match on the cc (email only) + */ + cc?: string[]; /** * The highlight match on the content field */ @@ -712,6 +780,18 @@ export interface UnifiedSearchOutput { * The highlight match on the name field */ name?: string | null; + /** + * The highlight match on the recipients (email only) + */ + recipients?: string[]; + /** + * The highlight match on the sender (email only) + */ + sender?: string | null; + /** + * The highlight match on the user (owner) of the entity + */ + user_id?: string | null; }; /** * The mentions @@ -748,6 +828,14 @@ export interface UnifiedSearchOutput { * The highlights on the project */ highlight: { + /** + * The highlight match on the bcc (email only) + */ + bcc?: string[]; + /** + * The highlight match on the cc (email only) + */ + cc?: string[]; /** * The highlight match on the content field */ @@ -756,6 +844,18 @@ export interface UnifiedSearchOutput { * The highlight match on the name field */ name?: string | null; + /** + * The highlight match on the recipients (email only) + */ + recipients?: string[]; + /** + * The highlight match on the sender (email only) + */ + sender?: string | null; + /** + * The highlight match on the user (owner) of the entity + */ + user_id?: string | null; }; /** * The project id @@ -802,17 +902,9 @@ export interface UnifiedSearchOutput { */ /** - * Trigger an intelligent internet search tool with a search query. Phrase the query as a natural language question. The search tool only has the information passed into the query string to include any relevant context from the conversation. Use this tool when the user specifically requests a web search, asks for resources, asks for links, asks you to read documentationWhen referencing links returned by search remember to use github markdown notation to format them like this [description](https://example.com)Our CEO describes when to use web search like this: I think some important criteria for choosing to search the web are", - * if the user is asking for time-sensitive information (sports scores, news, current happenings, etc) that would have changed since the knowledge cutoff date - * Specific questions that reference external sources, eg contain a hyperlink in the user message or explicitly say “use webmd” or “check arxiv” - * do not use web search for things that are likely to find SEO slop content; this will make it worse than if it didn’t do it - * the LLM is be allowed to guess an answer and then use the web search tool to check AFTER providing the answer, if it feels it’s necessary. E.g. user asks “when was Caesar stabbed” -> llm gives answer -> if needed use web to double check" - * do not use this tool many times in a single response, you think the user may need more information ask them before calling web search again. You should always sharethe results of your search with the user even if you are not sure if they are useful + * This is the expected shape of the streamed json following a `server_tool_use` in content_block_start event */ -export interface WebSearchInput { - /** - * The search string to search for. Should be long / descriptive - */ +export interface WebSearchToolCall { query: string; } @@ -824,11 +916,24 @@ export interface WebSearchInput { * and run json-schema-to-typescript to regenerate this file. */ -export interface WebSearchOutput { - content: string; - results: { - name: string; - url: string; - }[]; +/** + * A single search result from web search + */ +export type SearchResult = { + title: string; + type: 'web_search_result'; + url: string; +}; + +/** + * Web search response content returned by Claude when using the web_search tool + */ +export interface WebSearchResponse { + /** + * The search query that was executed + * Array of search results + */ + content: SearchResult[]; + tool_use_id: string; } diff --git a/js/app/packages/service-clients/service-cognition/toolTypes.ts b/js/app/packages/service-clients/service-cognition/toolTypes.ts deleted file mode 100644 index 08d01247b0..0000000000 --- a/js/app/packages/service-clients/service-cognition/toolTypes.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** Manually provided types for the schema files */ - -import type { FlattenArray } from '@core/util/flatten'; -import type * as types from './generated/tools/types'; - -export enum UnifiedSearchTypeEnum { - document = 'document', - chat = 'chat', - email = 'email', - channel = 'channel', - project = 'project', -} - -/** maps to a grouping of results for a given type - * e.g. a single document id match can have multiple content results - */ -export type UnifiedSearchResult = FlattenArray< - types.UnifiedSearchOutput['response']['results'] ->; - -export type ListDocumentsResult = FlattenArray< - types.ListDocumentsOutput['results'] ->; - -export type ListEmailsResult = FlattenArray; diff --git a/js/app/scripts/generate-dcs-tools.ts b/js/app/scripts/generate-dcs-tools.ts index e80a159c21..10f23bac7c 100644 --- a/js/app/scripts/generate-dcs-tools.ts +++ b/js/app/scripts/generate-dcs-tools.ts @@ -66,7 +66,7 @@ async function generateSchemasFile(tools: AiToolsResponse) { const { resolved: inputSchema } = await resolveRefs(tool.inputSchema); const inputCode = jsonSchemaToZod(inputSchema, { module: "esm", - name: `${tool.name}InputSchema`, + name: inputSchema.title, withoutDescribes: true, noImport: true, }); @@ -74,7 +74,7 @@ async function generateSchemasFile(tools: AiToolsResponse) { const { resolved: outputSchema } = await resolveRefs(tool.outputSchema); const outputCode = jsonSchemaToZod(outputSchema, { module: "esm", - name: `${tool.name}OutputSchema`, + name: outputSchema.title, withoutDescribes: true, noImport: true, }); @@ -97,14 +97,13 @@ async function generateToolTypesFile(tools: AiToolsResponse) { for (const tool of tools) { const inputCode = await compile( tool.inputSchema, - `${tool.name}InputSchema`, + tool.inputSchema.title, { // override top level name for export customName: (schema) => { if (schema.title) { - const newName = `${tool.name}Input`; - console.log(`Renamed ${schema.title} to ${newName}`); - return newName; + console.log(`Using schema title: ${schema.title}`); + return schema.title; } }, additionalProperties: false, @@ -116,14 +115,13 @@ async function generateToolTypesFile(tools: AiToolsResponse) { const outputCode = await compile( tool.outputSchema, - `${tool.name}OutputSchema`, + tool.outputSchema.title, { // override top level name for export customName: (schema) => { if (schema.title) { - const newName = `${tool.name}Output`; - console.log(`Renamed ${schema.title} to ${newName}`); - return newName; + console.log(`Using schema title: ${schema.title}`); + return schema.title; } return undefined; }, @@ -145,6 +143,19 @@ ${content.join("\n\n")} } async function generateToolsFile(tools: AiToolsResponse) { + // Resolve schemas to get titles + const toolsWithTitles = await Promise.all( + tools.map(async (tool) => { + const { resolved: inputSchema } = await resolveRefs(tool.inputSchema); + const { resolved: outputSchema } = await resolveRefs(tool.outputSchema); + return { + name: tool.name, + inputTitle: inputSchema.title, + outputTitle: outputSchema.title, + }; + }), + ); + const contents = `${warning} import { err, type MaybeResult, ok } from 'core/util/maybeResult'; @@ -153,19 +164,19 @@ import * as schemas from './schemas'; import type * as types from './types'; type ToolParserMap = { -${tools +${toolsWithTitles .map( (tool) => - `${tool.name}: { call: types.${tool.name}Input, response: types.${tool.name}Output }`, + `${tool.name}: { call: types.${tool.inputTitle}, response: types.${tool.outputTitle} }`, ) .join("\n")} }; const toolParserMap = { -${tools +${toolsWithTitles .map( (tool) => - `${tool.name}: { call: schemas.${tool.name}InputSchema, response: schemas.${tool.name}OutputSchema }`, + `${tool.name}: { call: schemas.${tool.inputTitle}, response: schemas.${tool.outputTitle} }`, ) .join(",\n")} }; @@ -179,10 +190,10 @@ type NamedRawTool = { }; type ToolDataMap = { -${tools +${toolsWithTitles .map( (tool) => - `${tool.name}: { call: types.${tool.name}Input, response: types.${tool.name}Output }`, + `${tool.name}: { call: types.${tool.inputTitle}, response: types.${tool.outputTitle} }`, ) .join(";\n")}; }; diff --git a/rust/cloud-storage/Cargo.lock b/rust/cloud-storage/Cargo.lock index 025e7b39d8..fdae22baa1 100644 --- a/rust/cloud-storage/Cargo.lock +++ b/rust/cloud-storage/Cargo.lock @@ -81,6 +81,7 @@ name = "ai_tools" version = "0.1.0" dependencies = [ "ai", + "anthropic", "anyhow", "async-trait", "chrono", diff --git a/rust/cloud-storage/ai/src/tool/mod.rs b/rust/cloud-storage/ai/src/tool/mod.rs index ea1d261d6a..0b492b7796 100644 --- a/rust/cloud-storage/ai/src/tool/mod.rs +++ b/rust/cloud-storage/ai/src/tool/mod.rs @@ -3,9 +3,5 @@ pub mod tool_loop; pub mod types; pub use tool_loop::ai_client::ToolLoop; -pub use types::AsyncTool; -pub use types::AsyncToolSet; -pub use types::Tool; -pub use types::ToolCallError; -pub use types::ToolResult; pub use types::tool_object::minimized_output_schema_generator; +pub use types::*; diff --git a/rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs b/rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs index d7ec470d2d..7b3d1fcc7e 100644 --- a/rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs +++ b/rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs @@ -3,7 +3,7 @@ use super::chat::Chat; use crate::tool::types::AsyncToolSet; use crate::types::AnthropicClient; use crate::types::ExtendedClient; -use anthropic::openai::request::AnthropicRequestExtensions; +use anthropic::openai::request::{AnthropicRequestExtension, AnthropicRequestExtensions}; use std::sync::Arc; pub struct ToolLoop @@ -12,10 +12,9 @@ where T: Clone + Send + Sync, R: Clone + Send + Sync, { - inner: I, + client: I, context: T, toolset: Arc>, - extensions: I::RequestExtension, } impl ToolLoop @@ -24,14 +23,13 @@ where R: Clone + Send + Sync, { pub fn new(toolset: AsyncToolSet, context: T) -> Self { - let client = AnthropicClient::new(); + let extensions = AnthropicRequestExtensions(vec![AnthropicRequestExtension::WebSearchTool]); + let client = AnthropicClient::new(extensions); let toolset = Arc::new(toolset); - let extensions = AnthropicRequestExtensions(vec![]); Self { - inner: client, + client, context, toolset, - extensions, } } } @@ -39,25 +37,22 @@ where impl ToolLoop where I: ExtendedClient + Clone + Send + Sync, - I::RequestExtension: Clone, T: Clone + Send + Sync, R: Clone + Send + Sync, { pub fn chat(&self) -> Chat { Chat::new( - self.inner.clone(), + self.client.clone(), self.toolset.clone(), self.context.clone(), - self.extensions.clone(), ) } pub fn chained(&self) -> Chained { Chained::new( - self.inner.clone(), + self.client.clone(), self.toolset.clone(), self.context.clone(), - self.extensions.clone(), ) } } diff --git a/rust/cloud-storage/ai/src/tool/tool_loop/chained.rs b/rust/cloud-storage/ai/src/tool/tool_loop/chained.rs index e4c02c0d15..174b8f58e3 100644 --- a/rust/cloud-storage/ai/src/tool/tool_loop/chained.rs +++ b/rust/cloud-storage/ai/src/tool/tool_loop/chained.rs @@ -1,14 +1,3 @@ -/// The chained API is identical to the chat API -/// -/// This loop doesn't send tool JSON schema to the selected model. -/// Instead this client sends (name, description) data of each tool, -/// then gives the primary model a tool to call a tool with -/// (name, instructions). This tool call is then used to send a single -/// tool to a secondary model that makes the tool call. -/// -/// This is done transparently so the calls to the `ChainedTool` are not -/// persisted to the database or presented to the frontend. This is -/// intended to be a drop-in replacement for the `Chat` client use crate::generate_tool_input_schema; use crate::tool::completion::tool_completion; use crate::tool::tool_loop::constant::{MAX_RECURSIONS, TOOL_GENERATOR}; @@ -51,6 +40,17 @@ struct ChainedTool { pub instructions: String, } +// The chained API is identical to the chat API +/// +/// This loop doesn't send tool JSON schema to the selected model. +/// Instead this client sends (name, description) data of each tool, +/// then gives the primary model a tool to call a tool with +/// (name, instructions). This tool call is then used to send a single +/// tool to a secondary model that makes the tool call. +/// +/// This is done transparently so the calls to the `ChainedTool` are not +/// persisted to the database or presented to the frontend. This is +/// intended to be a drop-in replacement for the `Chat` client impl ChainedTool { pub fn as_openai_tool() -> ChatCompletionTool { let schema = generate_tool_input_schema!(ChainedTool); @@ -99,7 +99,6 @@ where initial_message_count: usize, tool_call_id_name_mapping: HashMap, // tool_call_id -> tool_name tool_call_count: usize, - extensions: I::RequestExtension, } impl Chained @@ -108,12 +107,7 @@ where T: Clone + Send + Sync, R: Clone + Send + Sync, { - pub fn new( - client: I, - toolset: Arc>, - context: T, - extensions: I::RequestExtension, - ) -> Chained { + pub fn new(client: I, toolset: Arc>, context: T) -> Chained { Self { inner: client, toolset, @@ -123,7 +117,6 @@ where initial_message_count: 0, tool_call_id_name_mapping: HashMap::new(), tool_call_count: 0, - extensions, } } @@ -207,7 +200,7 @@ where }; { - let mut stream = Self::map_stream(&self.inner, stream, &mut self.request); + let mut stream = Self::map_stream(&self.inner, stream); // consume stream // accumulate to stream_parts @@ -359,7 +352,6 @@ where fn map_stream<'a>( client: &'a I, mut stream: ExtendedOpenAIStream, - request: &'a mut CreateChatCompletionRequest, ) -> ChatCompletionStream<'a> { Box::pin(stream!({ let mut tool_calls: HashMap = HashMap::new(); @@ -421,7 +413,7 @@ where } Ok(ExtendedOpenAIStreamItem::Extension(ext)) => { // Handle provider-specific extension items (Anthropic server tools) - if let Some(stream_part) = client.handle_extension_item(request, ext) { + if let Some(stream_part) = client.handle_extension_item(ext) { yield Ok(stream_part); } } @@ -444,8 +436,6 @@ where tracing::trace!("{:#?}", self.request); - self.inner - .chat_stream(self.request.clone(), &self.extensions) - .await + self.inner.chat_stream(self.request.clone()).await } } diff --git a/rust/cloud-storage/ai/src/tool/tool_loop/chat.rs b/rust/cloud-storage/ai/src/tool/tool_loop/chat.rs index 42ef7128d7..9e92472113 100644 --- a/rust/cloud-storage/ai/src/tool/tool_loop/chat.rs +++ b/rust/cloud-storage/ai/src/tool/tool_loop/chat.rs @@ -1,7 +1,6 @@ use super::constant::MAX_RECURSIONS; use crate::tool::types::{AsyncToolSet, PartialToolCall, StreamPart, ToolCall, ToolResult}; -use crate::tool::types::{ChatCompletionStream, ToolResponse}; - +use crate::tool::types::{ChatCompletionStream, ExtendedPartStream, PartOrExt, ToolResponse}; use crate::types::openai::message::convert_message; use crate::types::traits::{ExtendedOpenAIStream, ExtendedOpenAIStreamItem}; use crate::types::{ChatCompletionRequest, ChatMessage, ChatMessages}; @@ -28,7 +27,7 @@ where T: Clone + Send + Sync + 'static, R: Send + Sync + 'static, { - inner: I, + client: I, toolset: Arc>, request: CreateChatCompletionRequest, messages: Vec, @@ -36,7 +35,6 @@ where initial_message_count: usize, tool_call_id_name_mapping: HashMap, // tool_call_id -> tool_name user_id: String, - extensions: I::RequestExtension, } impl Chat @@ -45,14 +43,9 @@ where T: Clone + Send + Sync, R: Clone + Send + Sync, { - pub fn new( - client: I, - toolset: Arc>, - context: T, - extensions: I::RequestExtension, - ) -> Chat { + pub fn new(client: I, toolset: Arc>, context: T) -> Chat { Chat { - inner: client, + client, toolset, messages: vec![], context, @@ -60,7 +53,6 @@ where initial_message_count: 0, tool_call_id_name_mapping: HashMap::new(), user_id: "Uninitialized".into(), - extensions, } } @@ -103,19 +95,29 @@ where break; } }; - { - let mut stream = Self::map_stream(&self.inner, stream, &mut self.request); + let mut stream = Self::map_stream(stream); // consume stream // accumulate to stream_parts while let Some(item) = stream.next().await { - if item.is_err() { - yield item; + if let Err(e) = item { + yield Err(e); break; } - let stream_part = item.unwrap(); - yield Ok(stream_part.clone()); - stream_parts.push(stream_part); + + let part_or_ext = item.unwrap(); + match part_or_ext { + ref part @ PartOrExt::Part(ref p) => { + yield Ok(p.to_owned()); + stream_parts.push(part.to_owned()); + } + ref part @ PartOrExt::Ext(ref e) => { + if let Some(p) = self.client.handle_extension_item(e.to_owned()) { + yield Ok(p); + } + stream_parts.push(part.to_owned()); + } + } } } // call tools, aggregate response to a new request @@ -140,71 +142,112 @@ where async fn process_stream_parts( &mut self, - stream_parts: Vec, + stream_parts: Vec>, request_context: R, ) -> ProcessedStream { // list of all tool calls let mut tool_calls = vec![]; // list of all tool responses as openai items let mut tool_responses = vec![]; + // list of tool responses as openai items that are not returned / yielded + let mut non_yielding_responses = vec![]; // aggregated response string let mut response = String::new(); // list of tool responses as stream parts (send these to frontend) let mut tool_stream_parts = vec![]; for item in stream_parts { match item { - StreamPart::ToolCall(call) => { - // Store the tool call ID -> name mapping for later use in message conversion - self.tool_call_id_name_mapping - .insert(call.id.clone(), call.name.clone()); - - match self - .toolset - .try_tool_call( - self.context.clone(), - request_context.clone(), - &call.name, - &call.json, - ) - .await - { - Ok(response) => { - tool_calls.push(ChatCompletionMessageToolCall { - id: call.id.clone(), - r#type: async_openai::types::ChatCompletionToolType::Function, - function: FunctionCall { - arguments: call.json.to_string(), - name: call.name.clone(), - }, - }); - if let ToolResult::Ok(tool_output) = response { - let content_text = serde_json::to_string_pretty(&tool_output) - .unwrap_or_else(|_| { - "internal error formatting response".to_string() - }); - tool_stream_parts.push(ToolResponse::Json { + PartOrExt::Ext(ext) => { + if let Some(item) = self.client.handle_extension_item(ext) { + match item { + StreamPart::ToolCall(call) => { + // Store the tool call ID -> name mapping for later use in message conversion + self.tool_call_id_name_mapping + .insert(call.id.clone(), call.name.clone()); + tool_calls.push(ChatCompletionMessageToolCall { id: call.id.clone(), - json: tool_output, - name: call.name.clone(), + r#type: async_openai::types::ChatCompletionToolType::Function, + function: FunctionCall { + arguments: call.json.to_string(), + name: call.name.clone(), + }, }); - let content = - async_openai::types::ChatCompletionRequestToolMessageContent::Text( - content_text, + } + StreamPart::Content(text) => response.push_str(text.as_str()), + StreamPart::Usage { .. } => (), + StreamPart::ToolResponse(response) => { + if let ToolResponse::Json { id, json, .. } = response { + let content_text = serde_json::to_string_pretty(&json) + .unwrap_or_else(|_| "internal error parsing".into()); + let content = async_openai::types::ChatCompletionRequestToolMessageContent::Text( + content_text, + ); + non_yielding_responses.push( + ChatCompletionRequestMessage::Tool( + ChatCompletionRequestToolMessage { + content, + tool_call_id: id, + }, + ), ); - tool_responses.push(ChatCompletionRequestMessage::Tool( - ChatCompletionRequestToolMessage { - content, - tool_call_id: call.id, - }, - )); - } else { - let fail = response.unwrap_err(); - tool_stream_parts.push(ToolResponse::Err { + } + } + } + } + } + PartOrExt::Part(part) => match part { + StreamPart::ToolCall(call) => { + // Store the tool call ID -> name mapping for later use in message conversion + self.tool_call_id_name_mapping + .insert(call.id.clone(), call.name.clone()); + + match self + .toolset + .try_tool_call( + self.context.clone(), + request_context.clone(), + &call.name, + &call.json, + ) + .await + { + Ok(response) => { + tool_calls.push(ChatCompletionMessageToolCall { id: call.id.clone(), - description: fail.description.clone(), - name: call.name.clone(), + r#type: async_openai::types::ChatCompletionToolType::Function, + function: FunctionCall { + arguments: call.json.to_string(), + name: call.name.clone(), + }, }); - tool_responses.push(ChatCompletionRequestMessage::Tool( + if let ToolResult::Ok(tool_output) = response { + let content_text = serde_json::to_string_pretty(&tool_output) + .unwrap_or_else(|_| { + "internal error formatting response".to_string() + }); + tool_stream_parts.push(ToolResponse::Json { + id: call.id.clone(), + json: tool_output, + name: call.name.clone(), + }); + let content = + async_openai::types::ChatCompletionRequestToolMessageContent::Text( + content_text, + ); + tool_responses.push(ChatCompletionRequestMessage::Tool( + ChatCompletionRequestToolMessage { + content, + tool_call_id: call.id, + }, + )); + } else { + let fail = response.unwrap_err(); + tool_stream_parts.push(ToolResponse::Err { + id: call.id.clone(), + description: fail.description.clone(), + name: call.name.clone(), + }); + tool_responses.push(ChatCompletionRequestMessage::Tool( ChatCompletionRequestToolMessage { content: async_openai::types::ChatCompletionRequestToolMessageContent::Text( fail.description @@ -212,27 +255,27 @@ where tool_call_id: call.id }, )); + } } - } - Err(err) => { - tracing::error!(error=?err, "error calling tool"); - // Still add the tool call so the LLM knows we tried - tool_calls.push(ChatCompletionMessageToolCall { - id: call.id.clone(), - r#type: async_openai::types::ChatCompletionToolType::Function, - function: FunctionCall { - arguments: call.json.to_string(), + Err(err) => { + tracing::error!(error=?err, "error calling tool"); + // Still add the tool call so the LLM knows we tried + tool_calls.push(ChatCompletionMessageToolCall { + id: call.id.clone(), + r#type: async_openai::types::ChatCompletionToolType::Function, + function: FunctionCall { + arguments: call.json.to_string(), + name: call.name.clone(), + }, + }); + // Send error response to both frontend and LLM + let error_description = format!("Error calling tool: {}", err); + tool_stream_parts.push(ToolResponse::Err { + id: call.id.clone(), + description: error_description.clone(), name: call.name.clone(), - }, - }); - // Send error response to both frontend and LLM - let error_description = format!("Error calling tool: {}", err); - tool_stream_parts.push(ToolResponse::Err { - id: call.id.clone(), - description: error_description.clone(), - name: call.name.clone(), - }); - tool_responses.push(ChatCompletionRequestMessage::Tool( + }); + tool_responses.push(ChatCompletionRequestMessage::Tool( ChatCompletionRequestToolMessage { content: async_openai::types::ChatCompletionRequestToolMessageContent::Text( error_description @@ -240,13 +283,14 @@ where tool_call_id: call.id }, )); + } } } - } - StreamPart::Content(text) => response.push_str(text.as_str()), - StreamPart::Usage { .. } => (), - StreamPart::ToolResponse(_) => (), - }; + StreamPart::Content(text) => response.push_str(text.as_str()), + StreamPart::Usage { .. } => (), + StreamPart::ToolResponse(_) => (), + }, + } } let assistant_response = @@ -265,6 +309,7 @@ where }); let mut messages = vec![assistant_response]; messages.append(&mut tool_responses); + messages.append(&mut non_yielding_responses); ProcessedStream { new_messages: messages, tool_responses: tool_stream_parts, @@ -272,17 +317,15 @@ where } fn map_stream<'a>( - client: &'a I, mut stream: ExtendedOpenAIStream, - request: &'a mut CreateChatCompletionRequest, - ) -> ChatCompletionStream<'a> { - Box::pin(stream!({ + ) -> ExtendedPartStream<'a, I::ResponseExtension> { + let stream = stream!({ let mut tool_calls: HashMap = HashMap::new(); while let Some(item) = stream.next().await { match item { Ok(ExtendedOpenAIStreamItem::Response(part)) => { if let Some(usage) = &part.usage { - yield Ok(StreamPart::Usage(usage.clone().into())) + yield Ok(PartOrExt::Part(StreamPart::Usage(usage.clone().into()))) } let first = part.choices.first(); if first.is_none() { @@ -290,7 +333,7 @@ where } let first = first.unwrap(); if let Some(content) = &first.delta.content { - yield Ok(StreamPart::Content(content.clone())); + yield Ok(PartOrExt::Part(StreamPart::Content(content.clone()))); } if let Some(calls) = &first.delta.tool_calls { @@ -328,7 +371,9 @@ where if let Some(FinishReason::ToolCalls) = first.finish_reason { for call in tool_calls.into_values() { if let Ok(call) = ToolCall::try_from(call) { - yield Ok(StreamPart::ToolCall(call)); + yield Ok(PartOrExt::Part(StreamPart::ToolCall(call))); + } else { + panic!("Failed to try from") } } tool_calls = HashMap::new(); @@ -336,14 +381,13 @@ where } Ok(ExtendedOpenAIStreamItem::Extension(ext)) => { // Handle provider-specific extension items (Anthropic server tools) - if let Some(stream_part) = client.handle_extension_item(request, ext) { - yield Ok(stream_part); - } + yield Ok(PartOrExt::Ext(ext)); } Err(error) => yield Err(error.into()), } } - })) + }); + Box::pin(stream) } async fn make_openai_chat_completion_stream( @@ -356,9 +400,7 @@ where include_usage: true, }); - self.inner - .chat_stream(self.request.clone(), &self.extensions) - .await + self.client.chat_stream(self.request.clone()).await } } @@ -378,7 +420,7 @@ mod tests { fn create_mock_chat() -> Chat { let client = NoOpClient; let toolset = Arc::new(AsyncToolSet::new()); - Chat::new(client, toolset, "test_context".to_string(), ()) + Chat::new(client, toolset, "test_context".to_string()) } #[test] diff --git a/rust/cloud-storage/ai/src/tool/types/mod.rs b/rust/cloud-storage/ai/src/tool/types/mod.rs index 950eed3217..4723c98024 100644 --- a/rust/cloud-storage/ai/src/tool/types/mod.rs +++ b/rust/cloud-storage/ai/src/tool/types/mod.rs @@ -1,6 +1,7 @@ +pub mod schema; mod stream; -pub mod tool; -pub mod toolset; +mod tool; +mod toolset; pub use stream::*; pub use tool::*; diff --git a/rust/cloud-storage/ai/src/tool/types/schema/generate.rs b/rust/cloud-storage/ai/src/tool/types/schema/generate.rs new file mode 100644 index 0000000000..8b13413a7f --- /dev/null +++ b/rust/cloud-storage/ai/src/tool/types/schema/generate.rs @@ -0,0 +1,49 @@ +use crate::tool::AsyncToolSet; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ToolSchema { + pub name: String, + pub input_schema: serde_json::Value, + pub output_schema: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ToolSchemas { + pub schemas: Vec, +} + +pub trait ToolSchemaGenerator { + fn generate_schemas(&self) -> ToolSchemas; + fn merge(&self, generator: &dyn ToolSchemaGenerator) -> ToolSchemas { + let mut schemas = self.generate_schemas(); + schemas + .schemas + .append(&mut generator.generate_schemas().schemas); + schemas + } +} + +impl ToolSchemaGenerator for ToolSchemas { + fn generate_schemas(&self) -> ToolSchemas { + self.clone() + } +} + +impl ToolSchemaGenerator for AsyncToolSet { + fn generate_schemas(&self) -> ToolSchemas { + let schemas = self + .tools + .iter() + .map(|(name, tool_object)| ToolSchema { + name: name.clone(), + input_schema: tool_object.input_schema.clone(), + output_schema: tool_object.output_schema.clone(), + }) + .collect(); + ToolSchemas { schemas } + } +} diff --git a/rust/cloud-storage/ai/src/tool/types/schema/mod.rs b/rust/cloud-storage/ai/src/tool/types/schema/mod.rs new file mode 100644 index 0000000000..884ad437d2 --- /dev/null +++ b/rust/cloud-storage/ai/src/tool/types/schema/mod.rs @@ -0,0 +1,5 @@ +mod generate; +mod phantom_tool; + +pub use generate::*; +pub use phantom_tool::*; diff --git a/rust/cloud-storage/ai/src/tool/types/schema/phantom_tool.rs b/rust/cloud-storage/ai/src/tool/types/schema/phantom_tool.rs new file mode 100644 index 0000000000..24b905a8a0 --- /dev/null +++ b/rust/cloud-storage/ai/src/tool/types/schema/phantom_tool.rs @@ -0,0 +1,79 @@ +use super::generate::{ToolSchema, ToolSchemaGenerator, ToolSchemas}; +use schemars::{JsonSchema, schema_for}; +use std::fmt::Debug; +use std::marker::PhantomData; + +/// A tool that is not sent to AI but may be called by ai (built in tools) +/// Generate schemas for these tools for the frontend +#[derive(Clone, Debug)] +pub struct PhantomTool { + i: PhantomData, + o: PhantomData, + pub name: &'static str, +} + +impl PhantomTool { + pub fn new(name: &'static str) -> Self { + PhantomTool { + i: PhantomData, + o: PhantomData, + name, + } + } +} + +impl PhantomTool<(), ()> { + pub fn builder(name: &'static str) -> Self { + PhantomTool { + i: PhantomData, + o: PhantomData, + name, + } + } +} + +impl PhantomTool<(), O> { + pub fn with_input_schema(self) -> PhantomTool + where + I: JsonSchema, + { + PhantomTool { + i: PhantomData, + o: PhantomData, + name: self.name, + } + } +} + +impl PhantomTool { + pub fn with_output_schema(self) -> PhantomTool + where + O: JsonSchema, + { + PhantomTool { + i: PhantomData, + o: PhantomData, + name: self.name, + } + } +} + +impl ToolSchemaGenerator for PhantomTool +where + I: JsonSchema + Clone + Debug, + O: JsonSchema + Clone + Debug, +{ + fn generate_schemas(&self) -> ToolSchemas { + let input_schema = schema_for!(I); + let output_schema = schema_for!(O); + let input_schema_json = serde_json::to_value(&input_schema).expect("input schema"); + let output_schema_json = serde_json::to_value(&output_schema).expect("output schema"); + ToolSchemas { + schemas: vec![ToolSchema { + name: self.name.to_owned(), + input_schema: input_schema_json, + output_schema: output_schema_json, + }], + } + } +} diff --git a/rust/cloud-storage/ai/src/tool/types/stream.rs b/rust/cloud-storage/ai/src/tool/types/stream.rs index 0d9de1d4ab..482d33c6a1 100644 --- a/rust/cloud-storage/ai/src/tool/types/stream.rs +++ b/rust/cloud-storage/ai/src/tool/types/stream.rs @@ -1,10 +1,18 @@ use crate::types::{AiError, Usage}; use futures::stream::Stream; use serde::Serialize; +use std::fmt::Debug; use std::pin::Pin; -pub type ChatCompletionStream<'a> = - Pin> + Send + 'a>>; +pub type AiStream<'a, T> = Pin> + Send + 'a>>; +pub type ChatCompletionStream<'a> = AiStream<'a, StreamPart>; +pub(crate) type ExtendedPartStream<'a, T> = AiStream<'a, PartOrExt>; + +#[derive(Debug, Clone)] +pub(crate) enum PartOrExt { + Part(StreamPart), + Ext(T), +} #[derive(Debug, Clone)] pub enum StreamPart { diff --git a/rust/cloud-storage/ai/src/tool/types/tool.rs b/rust/cloud-storage/ai/src/tool/types/tool.rs index 86e32591f0..571b0a74b3 100644 --- a/rust/cloud-storage/ai/src/tool/types/tool.rs +++ b/rust/cloud-storage/ai/src/tool/types/tool.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use serde::Serialize; pub type ToolResult = std::result::Result; +pub struct NoContext(); #[derive(Debug)] pub struct ToolCallError { @@ -29,5 +30,3 @@ pub trait AsyncTool: Sync + Send { type Output: Serialize + 'static; async fn call(&self, service_context: Sc, request_context: Rc) -> ToolResult; } - -pub struct NoContext(); diff --git a/rust/cloud-storage/ai/src/tool/types/toolset/mod.rs b/rust/cloud-storage/ai/src/tool/types/toolset/mod.rs index d7cb5669a6..e8d782a1d7 100644 --- a/rust/cloud-storage/ai/src/tool/types/toolset/mod.rs +++ b/rust/cloud-storage/ai/src/tool/types/toolset/mod.rs @@ -1,175 +1,3 @@ pub mod tool_object; -pub mod types; - -use crate::tool::types::{AsyncTool, Tool, ToolResult}; -use async_openai::types::ChatCompletionTool; -use schemars::{JsonSchema, Schema}; -use serde::Serialize; -use serde::de::Deserialize; -use std::collections::hash_map::HashMap; -use tool_object::{AsyncToolObject, SyncToolObject}; -use types::*; - -pub type SyncToolSet = ToolSet>; - -pub type AsyncToolSet = ToolSet>; - -pub struct ToolSchema { - pub name: String, - pub schema: Schema, - pub result_schema: Schema, -} - -impl ToolSchema { - pub fn new(name: String, schema: Schema, result_schema: Schema) -> Self { - Self { - name, - schema, - result_schema, - } - } -} - -#[derive(Default)] -pub struct ToolSet { - pub tools: HashMap, -} - -impl ToolSet { - pub fn new() -> Self { - Self { - tools: HashMap::new(), - } - } -} - -impl ToolSet> -where - Rc: Sync + Send + 'static, - Sc: Sync + Send + 'static, -{ - pub fn add_tool(mut self) -> Result - where - T: JsonSchema + Tool + for<'de> Deserialize<'de> + 'static + Send + Sync, - T::Output: Serialize + JsonSchema + 'static, - { - let tool_object = - SyncToolObject::try_from_tool::().map_err(ToolSetCreationError::Validation)?; - if self.tools.contains_key(&tool_object.name) { - Err(ToolSetCreationError::NameConflict(tool_object.name.clone())) - } else { - self.tools.insert(tool_object.name.clone(), tool_object); - Ok(self) - } - } - - pub fn try_tool_call( - &self, - context: Sc, - request_context: Rc, - tool_name: &str, - json: &serde_json::Value, - ) -> Result, ToolCallError> { - let tool = self - .tools - .get(tool_name) - .ok_or_else(|| ToolCallError::NotFound(tool_name.to_owned())) - .and_then(|tool| { - tool.try_deserialize(json) - .map_err(ToolCallError::Deserialization) - })?; - Ok(tool.call(context, request_context)) - } -} - -impl ToolSet { - pub fn add_toolset(mut self, toolset: ToolSet) -> Result { - for (name, _) in toolset.tools.iter() { - if self.tools.contains_key(name) { - return Err(ToolSetCreationError::NameConflict(name.clone())); - } - } - self.tools.extend(toolset.tools); - Ok(self) - } -} - -impl SyncToolSet -where - Sc: Send + Sync + 'static, - Rc: Send + Sync + 'static, -{ - pub fn into_async(self) -> AsyncToolSet { - AsyncToolSet { - tools: self - .tools - .into_iter() - .map(|(name, obj)| (name, AsyncToolObject::from(obj))) - .collect(), - } - } -} - -impl AsyncToolSet -where - Rc: Sync + Send + 'static, - Sc: Sync + Send + 'static, -{ - pub fn add_tool(mut self) -> Result - where - T: JsonSchema + AsyncTool + for<'de> Deserialize<'de> + 'static + Send + Sync, - T::Output: Serialize + JsonSchema + 'static, - { - let tool_object = AsyncToolObject::try_from_tool::() - .map_err(ToolSetCreationError::Validation)?; - if self.tools.contains_key(&tool_object.name) { - Err(ToolSetCreationError::NameConflict(tool_object.name.clone())) - } else { - self.tools.insert(tool_object.name.clone(), tool_object); - Ok(self) - } - } - - pub async fn try_tool_call( - &self, - context: Sc, - request_context: Rc, - tool_name: &str, - json: &serde_json::Value, - ) -> Result, ToolCallError> { - let tool = self - .tools - .get(tool_name) - .ok_or_else(|| ToolCallError::NotFound(tool_name.to_owned())) - .and_then(|tool| { - tool.try_deserialize(json) - .map_err(ToolCallError::Deserialization) - })?; - Ok(tool.call(context, request_context).await) - } -} - -impl ToolSet -where - ChatCompletionTool: for<'a> From<&'a T>, -{ - pub fn openai_chatcompletion_toolset(&self) -> Vec { - self.tools.values().map(ChatCompletionTool::from).collect() - } -} - -impl std::fmt::Debug for ToolSet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut list = f.debug_list(); - list.entries(self.tools.keys()); - list.finish() - } -} - -impl Clone for ToolSet { - fn clone(&self) -> Self { - Self { - tools: self.tools.clone(), - } - } -} +mod types; +pub use types::*; diff --git a/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/json_tool.rs b/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/json_tool.rs index da7f934607..0452488b70 100644 --- a/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/json_tool.rs +++ b/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/json_tool.rs @@ -1,4 +1,5 @@ -use crate::tool::{AsyncTool, Tool, ToolCallError, ToolResult}; +use crate::tool::types::ToolCallError; +use crate::tool::{AsyncTool, Tool, ToolResult}; pub struct JsonTool(Box>); diff --git a/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/util.rs b/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/util.rs index 378853a56c..a68e8e1fe3 100644 --- a/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/util.rs +++ b/rust/cloud-storage/ai/src/tool/types/toolset/tool_object/util.rs @@ -195,7 +195,7 @@ pub fn output_schema_generator() -> SchemaGenerator { #[macro_export] macro_rules! generate_tool_input_schema { ($tool:ty) => {{ - use $crate::tool::types::toolset::tool_object::input_schema_generator; + use $crate::tool::types::tool_object::input_schema_generator; input_schema_generator().into_root_schema_for::<$tool>() }}; } @@ -203,7 +203,7 @@ macro_rules! generate_tool_input_schema { #[macro_export] macro_rules! generate_tool_output_schema { ($tool:ty) => {{ - use $crate::tool::types::toolset::tool_object::output_schema_generator; + use $crate::tool::types::tool_object::output_schema_generator; output_schema_generator().into_root_schema_for::<$tool>() }}; } diff --git a/rust/cloud-storage/ai/src/tool/types/toolset/types.rs b/rust/cloud-storage/ai/src/tool/types/toolset/types.rs index 249043b29a..6a4ee08536 100644 --- a/rust/cloud-storage/ai/src/tool/types/toolset/types.rs +++ b/rust/cloud-storage/ai/src/tool/types/toolset/types.rs @@ -1,4 +1,10 @@ -use super::tool_object::ValidationError; +use super::tool_object::{AsyncToolObject, SyncToolObject, ValidationError}; +use crate::tool::{AsyncTool, Tool, ToolResult}; +use async_openai::types::ChatCompletionTool; +use schemars::{JsonSchema, Schema}; +use serde::Serialize; +use serde::de::Deserialize; +use std::collections::hash_map::HashMap; use thiserror::Error; #[derive(Debug, Error)] @@ -10,9 +16,173 @@ pub enum ToolSetCreationError { } #[derive(Debug, Error)] -pub enum ToolCallError { +pub enum ToolSetError { #[error("error deserializing tool call (possible hallucination)")] Deserialization(serde_json::Error), #[error("tool not in toolset")] NotFound(String), } + +pub type SyncToolSet = ToolSet>; + +pub type AsyncToolSet = ToolSet>; + +pub struct ToolSchema { + pub name: String, + pub schema: Schema, + pub result_schema: Schema, +} + +impl ToolSchema { + pub fn new(name: String, schema: Schema, result_schema: Schema) -> Self { + Self { + name, + schema, + result_schema, + } + } +} + +#[derive(Default)] +pub struct ToolSet { + pub tools: HashMap, +} + +impl ToolSet { + pub fn new() -> Self { + Self { + tools: HashMap::new(), + } + } +} + +impl ToolSet> +where + Rc: Sync + Send + 'static, + Sc: Sync + Send + 'static, +{ + pub fn add_tool(mut self) -> Result + where + T: JsonSchema + Tool + for<'de> Deserialize<'de> + 'static + Send + Sync, + T::Output: Serialize + JsonSchema + 'static, + { + let tool_object = + SyncToolObject::try_from_tool::().map_err(ToolSetCreationError::Validation)?; + if self.tools.contains_key(&tool_object.name) { + Err(ToolSetCreationError::NameConflict(tool_object.name.clone())) + } else { + self.tools.insert(tool_object.name.clone(), tool_object); + Ok(self) + } + } + + pub fn try_tool_call( + &self, + context: Sc, + request_context: Rc, + tool_name: &str, + json: &serde_json::Value, + ) -> Result, ToolSetError> { + let tool = self + .tools + .get(tool_name) + .ok_or_else(|| ToolSetError::NotFound(tool_name.to_owned())) + .and_then(|tool| { + tool.try_deserialize(json) + .map_err(ToolSetError::Deserialization) + })?; + Ok(tool.call(context, request_context)) + } +} + +impl ToolSet { + pub fn add_toolset(mut self, toolset: ToolSet) -> Result { + for (name, _) in toolset.tools.iter() { + if self.tools.contains_key(name) { + return Err(ToolSetCreationError::NameConflict(name.clone())); + } + } + self.tools.extend(toolset.tools); + Ok(self) + } +} + +impl SyncToolSet +where + Sc: Send + Sync + 'static, + Rc: Send + Sync + 'static, +{ + pub fn into_async(self) -> AsyncToolSet { + AsyncToolSet { + tools: self + .tools + .into_iter() + .map(|(name, obj)| (name, AsyncToolObject::from(obj))) + .collect(), + } + } +} + +impl AsyncToolSet +where + Rc: Sync + Send + 'static, + Sc: Sync + Send + 'static, +{ + pub fn add_tool(mut self) -> Result + where + T: JsonSchema + AsyncTool + for<'de> Deserialize<'de> + 'static + Send + Sync, + T::Output: Serialize + JsonSchema + 'static, + { + let tool_object = AsyncToolObject::try_from_tool::() + .map_err(ToolSetCreationError::Validation)?; + if self.tools.contains_key(&tool_object.name) { + Err(ToolSetCreationError::NameConflict(tool_object.name.clone())) + } else { + self.tools.insert(tool_object.name.clone(), tool_object); + Ok(self) + } + } + + pub async fn try_tool_call( + &self, + context: Sc, + request_context: Rc, + tool_name: &str, + json: &serde_json::Value, + ) -> Result, ToolSetError> { + let tool = self + .tools + .get(tool_name) + .ok_or_else(|| ToolSetError::NotFound(tool_name.to_owned())) + .and_then(|tool| { + tool.try_deserialize(json) + .map_err(ToolSetError::Deserialization) + })?; + Ok(tool.call(context, request_context).await) + } +} + +impl ToolSet +where + ChatCompletionTool: for<'a> From<&'a T>, +{ + pub fn openai_chatcompletion_toolset(&self) -> Vec { + self.tools.values().map(ChatCompletionTool::from).collect() + } +} + +impl std::fmt::Debug for ToolSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut list = f.debug_list(); + list.entries(self.tools.keys()); + list.finish() + } +} + +impl Clone for ToolSet { + fn clone(&self) -> Self { + Self { + tools: self.tools.clone(), + } + } +} diff --git a/rust/cloud-storage/ai/src/types/client/anthropic.rs b/rust/cloud-storage/ai/src/types/client/anthropic.rs index e230f81665..ffce467a3e 100644 --- a/rust/cloud-storage/ai/src/types/client/anthropic.rs +++ b/rust/cloud-storage/ai/src/types/client/anthropic.rs @@ -1,6 +1,6 @@ use super::{ExtendedClient, ExtendedOpenAIStreamItem}; use crate::{ - tool::types::{StreamPart, ToolCall, ToolResponse}, + tool::{StreamPart, ToolResponse, types::ToolCall}, types::AiError, }; use anthropic::openai::{ @@ -12,18 +12,22 @@ use futures::StreamExt; #[derive(Clone, Debug)] pub struct AnthropicClient { inner: anthropic::client::Client, + extensions: AnthropicRequestExtensions, } impl AnthropicClient { - pub fn new() -> Self { + pub fn new(extensions: AnthropicRequestExtensions) -> Self { let client = anthropic::client::Client::dangerously_try_from_env(); - Self { inner: client } + Self { + inner: client, + extensions, + } } } impl Default for AnthropicClient { fn default() -> Self { - Self::new() + Self::new(AnthropicRequestExtensions(vec![])) } } @@ -37,45 +41,38 @@ impl From for ExtendedOpenAIStreamItem anyhow::Result, AiError> { Ok(Box::pin( self.inner .chat() - .create_stream_openai_extended(request, extensions) + .create_stream_openai_extended(request, &self.extensions) .await .map(|f| f.map(ExtendedOpenAIStreamItem::from)), )) } - // TODO: this is incomplete - // The request must be updated to record the call / response to correctly save / load chat - // This won't be hit until extensions are enabled in the next pr - fn handle_extension_item( - &self, - _: &mut async_openai::types::CreateChatCompletionRequest, - item: Self::ResponseExtension, - ) -> Option { + fn handle_extension_item(&self, item: Self::ResponseExtension) -> Option { match item { AnthropicResponseExtension::Citation(_) => None, AnthropicResponseExtension::ServerToolUse(tool_call) => { Some(StreamPart::ToolCall(ToolCall { id: tool_call.id, - json: serde_json::Value::Null, + json: tool_call.input, name: tool_call.name, })) } AnthropicResponseExtension::WebSearchToolResponse(response) => { + let id = response.tool_use_id.clone(); + let json = serde_json::to_value(&response).ok()?; + Some(StreamPart::ToolResponse(ToolResponse::Json { - id: "searchington".into(), - json: serde_json::to_value(&response.content).unwrap(), - name: "web-search".into(), + id, + json, + name: "web_search".into(), })) } } diff --git a/rust/cloud-storage/ai/src/types/client/noop.rs b/rust/cloud-storage/ai/src/types/client/noop.rs index 67419e6c3a..f06ba0c87c 100644 --- a/rust/cloud-storage/ai/src/types/client/noop.rs +++ b/rust/cloud-storage/ai/src/types/client/noop.rs @@ -1,5 +1,5 @@ use super::traits::{ExtendedClient, ExtendedOpenAIStream}; -use crate::tool::types::StreamPart; +use crate::tool::StreamPart; use crate::types::AiError; use anyhow::anyhow; @@ -10,22 +10,16 @@ pub struct NoOpClient; pub enum NoOpExtension {} impl ExtendedClient for NoOpClient { - type RequestExtension = (); type ResponseExtension = NoOpExtension; async fn chat_stream( &self, _: async_openai::types::CreateChatCompletionRequest, - _: &Self::RequestExtension, ) -> Result, AiError> { Err(anyhow!("noop").into()) } - fn handle_extension_item( - &self, - _: &mut async_openai::types::CreateChatCompletionRequest, - _: Self::ResponseExtension, - ) -> Option { + fn handle_extension_item(&self, _: Self::ResponseExtension) -> Option { None } } diff --git a/rust/cloud-storage/ai/src/types/client/openrouter/client.rs b/rust/cloud-storage/ai/src/types/client/openrouter/client.rs index 07aa2bf567..36d9dc9544 100644 --- a/rust/cloud-storage/ai/src/types/client/openrouter/client.rs +++ b/rust/cloud-storage/ai/src/types/client/openrouter/client.rs @@ -1,4 +1,5 @@ use super::config::OpenRouterConfig; +use crate::tool::types::StreamPart; use crate::types::client::traits::ExtendedClient; use crate::types::{ AiError, ExtendedOpenAIStream, ExtendedOpenAIStreamItem, Model, ModelWithMetadataAndProvider, @@ -48,13 +49,11 @@ impl OpenRouterClient { impl ExtendedClient for OpenRouterClient { // not yet implemented - type RequestExtension = (); type ResponseExtension = (); async fn chat_stream( &self, request: CreateChatCompletionRequest, - _: &Self::RequestExtension, ) -> anyhow::Result, AiError> { let request = self.preprocess_request(request); self.inner @@ -70,11 +69,7 @@ impl ExtendedClient for OpenRouterClient { } // extensions are not yet supported so this will never be called - fn handle_extension_item( - &self, - _: &mut CreateChatCompletionRequest, - _: Self::ResponseExtension, - ) -> Option { + fn handle_extension_item(&self, _: Self::ResponseExtension) -> Option { None } } diff --git a/rust/cloud-storage/ai/src/types/client/traits.rs b/rust/cloud-storage/ai/src/types/client/traits.rs index 826d414d3a..4cdb287a07 100644 --- a/rust/cloud-storage/ai/src/types/client/traits.rs +++ b/rust/cloud-storage/ai/src/types/client/traits.rs @@ -1,13 +1,15 @@ -use crate::tool::types::StreamPart; +use crate::tool::StreamPart; use crate::types::AiError; use anyhow::Result; use async_openai::error::OpenAIError; use async_openai::types::{CreateChatCompletionRequest, CreateChatCompletionStreamResponse}; use futures::Stream; +use std::fmt::Debug; use std::future::Future; use std::pin::Pin; -pub enum ExtendedOpenAIStreamItem { +#[derive(Debug, Clone)] +pub enum ExtendedOpenAIStreamItem { /// A standard OpenAI compatible item Response(CreateChatCompletionStreamResponse), /// A client-defined item @@ -20,17 +22,11 @@ pub type ExtendedOpenAIStream = /// A client that is openai compatible may implement this trait. /// Extension items may be used to support non-openai compatible featuture (ie server tools) pub trait ExtendedClient { - type RequestExtension: Send; - type ResponseExtension: Send; + type ResponseExtension: Send + Sync + Clone + Debug + 'static; fn chat_stream( &self, request: CreateChatCompletionRequest, - extensions: &Self::RequestExtension, ) -> impl Future, AiError>> + Send; - fn handle_extension_item( - &self, - request: &mut CreateChatCompletionRequest, - item: Self::ResponseExtension, - ) -> Option; + fn handle_extension_item<'a>(&self, item: Self::ResponseExtension) -> Option; } diff --git a/rust/cloud-storage/ai_tools/Cargo.toml b/rust/cloud-storage/ai_tools/Cargo.toml index e542489746..ce91ae2596 100644 --- a/rust/cloud-storage/ai_tools/Cargo.toml +++ b/rust/cloud-storage/ai_tools/Cargo.toml @@ -38,5 +38,7 @@ tracing.workspace = true utoipa.workspace = true uuid.workspace = true +anthropic = { path = "../anthropic"} + [dev-dependencies] cool_asserts = "2" diff --git a/rust/cloud-storage/ai_tools/src/lib.rs b/rust/cloud-storage/ai_tools/src/lib.rs index ac90e9e029..b735bc024f 100644 --- a/rust/cloud-storage/ai_tools/src/lib.rs +++ b/rust/cloud-storage/ai_tools/src/lib.rs @@ -1,10 +1,12 @@ use ai::tool::AsyncToolSet; +use ai::tool::schema::{ToolSchemaGenerator, ToolSchemas}; pub mod list; pub mod prompts; pub mod read; pub mod rewrite; pub mod search; mod tool_context; +use search::anthropic_web_search::anthropic_web_search_tool; pub use search::search_toolset; pub use tool_context::*; @@ -18,6 +20,12 @@ pub struct ToolSetWithPrompt { pub prompt: &'static str, } +impl ToolSchemaGenerator for ToolSetWithPrompt { + fn generate_schemas(&self) -> ai::tool::schema::ToolSchemas { + self.toolset.generate_schemas() + } +} + pub fn all_tools() -> ToolSetWithPrompt { let toolset = AsyncToolSet::new() .add_toolset(search_toolset()) @@ -32,6 +40,12 @@ pub fn all_tools() -> ToolSetWithPrompt { ToolSetWithPrompt { toolset, prompt } } +pub fn all_tool_schemas() -> ToolSchemas { + all_tools() + .merge(&*anthropic_web_search_tool) + .generate_schemas() +} + pub fn no_tools() -> ToolSetWithPrompt { ToolSetWithPrompt { prompt: prompts::BASE_PROMPT, diff --git a/rust/cloud-storage/ai_tools/src/list/email/tests.rs b/rust/cloud-storage/ai_tools/src/list/email/tests.rs index a352530082..0ab12b4f1d 100644 --- a/rust/cloud-storage/ai_tools/src/list/email/tests.rs +++ b/rust/cloud-storage/ai_tools/src/list/email/tests.rs @@ -1,6 +1,6 @@ use super::tool::*; use ai::generate_tool_input_schema; -use ai::tool::types::toolset::tool_object::validate_tool_schema; +use ai::tool::types::tool_object::validate_tool_schema; use cool_asserts::assert_matches; use email::inbound::ApiPaginatedThreadCursor; use models_email::service::thread::PreviewViewStandardLabel; diff --git a/rust/cloud-storage/ai_tools/src/list/file.rs b/rust/cloud-storage/ai_tools/src/list/file.rs index e665377695..2a8cb8e979 100644 --- a/rust/cloud-storage/ai_tools/src/list/file.rs +++ b/rust/cloud-storage/ai_tools/src/list/file.rs @@ -245,7 +245,7 @@ impl AsyncTool for ListDocuments { mod tests { use super::*; use ai::generate_tool_input_schema; - use ai::tool::types::toolset::tool_object::validate_tool_schema; + use ai::tool::types::tool_object::validate_tool_schema; // run `cargo test -p ai_tools list::file::tests::print_input_schema -- --nocapture --include-ignored` #[test] diff --git a/rust/cloud-storage/ai_tools/src/read.rs b/rust/cloud-storage/ai_tools/src/read.rs index c2089475a3..9f33e65c59 100644 --- a/rust/cloud-storage/ai_tools/src/read.rs +++ b/rust/cloud-storage/ai_tools/src/read.rs @@ -487,7 +487,7 @@ impl Read { #[cfg(test)] mod tests { use super::*; - use ai::tool::types::toolset::tool_object::validate_tool_schema; + use ai::tool::types::tool_object::validate_tool_schema; use ai::{generate_tool_input_schema, generate_tool_output_schema}; // run `cargo test -p ai_tools read::tests::print_input_schema -- --nocapture --include-ignored` diff --git a/rust/cloud-storage/ai_tools/src/search/anthropic_web_search.rs b/rust/cloud-storage/ai_tools/src/search/anthropic_web_search.rs new file mode 100644 index 0000000000..e8832da75f --- /dev/null +++ b/rust/cloud-storage/ai_tools/src/search/anthropic_web_search.rs @@ -0,0 +1,8 @@ +use ai::tool::schema::PhantomTool; +use anthropic::types::response::web_search::{WebSearchResponse, WebSearchToolCall}; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref anthropic_web_search_tool: PhantomTool = + PhantomTool::new("web_search"); +} diff --git a/rust/cloud-storage/ai_tools/src/search/mod.rs b/rust/cloud-storage/ai_tools/src/search/mod.rs index ebc557b99c..59ef547997 100644 --- a/rust/cloud-storage/ai_tools/src/search/mod.rs +++ b/rust/cloud-storage/ai_tools/src/search/mod.rs @@ -1,13 +1,18 @@ use crate::AiToolSet; use ai::tool::AsyncToolSet; +#[allow(unused)] +mod perplexity_search; + +/// Schemas for frontend type generation for the builtin claude web search tool +/// +/// This tool is built into anthropic so is not included in the toolset / sent in the request +pub mod anthropic_web_search; + mod unified; -mod web; pub fn search_toolset() -> AiToolSet { AsyncToolSet::new() .add_tool::() .expect("failed to add unified search tool") - .add_tool::() - .expect("fialed to add web search tool") } diff --git a/rust/cloud-storage/ai_tools/src/search/web.rs b/rust/cloud-storage/ai_tools/src/search/perplexity_search.rs similarity index 96% rename from rust/cloud-storage/ai_tools/src/search/web.rs rename to rust/cloud-storage/ai_tools/src/search/perplexity_search.rs index 45a50685a4..424dcc3088 100644 --- a/rust/cloud-storage/ai_tools/src/search/web.rs +++ b/rust/cloud-storage/ai_tools/src/search/perplexity_search.rs @@ -42,13 +42,13 @@ const WEB_SEARCH_SYSTEM_PROMPT: &str = concat!( #[schemars( description = WEB_SEARCH_DESCRIPTION )] -pub struct WebSearch { +pub struct PerplexitySearch { #[schemars(description = "The search string to search for. Should be long / descriptive")] pub query: String, } #[async_trait] -impl AsyncTool for WebSearch { +impl AsyncTool for PerplexitySearch { type Output = SearchResults; #[tracing::instrument(skip_all, fields(user_id=?_request_context.user_id), err)] @@ -57,8 +57,6 @@ impl AsyncTool for WebSearch { _service_context: ToolServiceContext, _request_context: RequestContext, ) -> ToolResult { - tracing::info!(self=?self, "Web search params"); - let client = ai::web_search::PerplexityClient::from_env().map_err(|err| ToolCallError { description: "Search failed due to an internal error. Do not try to search again." .to_string(), diff --git a/rust/cloud-storage/ai_tools/src/search/unified.rs b/rust/cloud-storage/ai_tools/src/search/unified.rs index a8f1978594..73e7ba0508 100644 --- a/rust/cloud-storage/ai_tools/src/search/unified.rs +++ b/rust/cloud-storage/ai_tools/src/search/unified.rs @@ -323,7 +323,7 @@ impl AsyncTool for UnifiedSearch { mod tests { use super::*; use ai::generate_tool_input_schema; - use ai::tool::types::toolset::tool_object::validate_tool_schema; + use ai::tool::types::tool_object::validate_tool_schema; // run `cargo test -p ai_tools unified::tests::print_output_schema -- --nocapture --include-ignored` #[test] diff --git a/rust/cloud-storage/anthropic/examples/server_tool.rs b/rust/cloud-storage/anthropic/examples/server_tool.rs index f272227a0d..10c09efbf6 100644 --- a/rust/cloud-storage/anthropic/examples/server_tool.rs +++ b/rust/cloud-storage/anthropic/examples/server_tool.rs @@ -8,6 +8,9 @@ use anthropic::types::request::{ }; use anthropic::types::response::{ContentDeltaEvent, StreamEvent}; use futures::StreamExt; +use std::fs::OpenOptions; + +const LOG_MODE: bool = false; #[tokio::main] async fn main() { @@ -36,18 +39,23 @@ async fn main() { let mut stream = chat.create_stream(request.clone()).await; let mut assistant_message = String::new(); - // let mut out = OpenOptions::new() - // .write(true) - // .create(true) - // .open("stream.json") - // .unwrap(); + let mut file = OpenOptions::new() + .write(true) + .create(LOG_MODE) + .open("stream.json") + .ok(); while let Some(event) = stream.next().await { - // if let Ok(e) = event { - // write!(out, "\n{}\n", serde_json::to_string_pretty(&e).unwrap()); - // } else { - // write!(out, "{:#?}", event.unwrap_err()); - // } + if LOG_MODE { + if let Some(ref mut file) = file { + if let Ok(e) = event { + write!(file, "\n{}\n", serde_json::to_string_pretty(&e).unwrap()).unwrap(); + } else { + write!(file, "{:#?}", event.unwrap_err()).unwrap(); + } + } + continue; + } if let Err(error) = event { match error { other => { diff --git a/rust/cloud-storage/anthropic/src/openai/stream_extension.rs b/rust/cloud-storage/anthropic/src/openai/stream_extension.rs index c4cf7b1d10..6cce2e146e 100644 --- a/rust/cloud-storage/anthropic/src/openai/stream_extension.rs +++ b/rust/cloud-storage/anthropic/src/openai/stream_extension.rs @@ -1,4 +1,6 @@ -use crate::types::response::{Citation, ServerToolUse, WebSearchResponse}; +use crate::prelude::ServerToolUse; +use crate::types::response::Citation; +use crate::types::response::web_search::WebSearchResponse; use async_openai::error::OpenAIError; use async_openai::types::CreateChatCompletionStreamResponse; use futures::Stream; @@ -7,9 +9,9 @@ use std::pin::Pin; /// Items that are returned in an Anthropic stream but not supported by OpenAI #[derive(Clone, Debug, PartialEq)] pub enum AnthropicResponseExtension { - ServerToolUse(ServerToolUse), Citation(Citation), WebSearchToolResponse(WebSearchResponse), + ServerToolUse(ServerToolUse), } /// A standard OpenAI response item or an item only sent by Anthropic diff --git a/rust/cloud-storage/anthropic/src/openai/stream_response.rs b/rust/cloud-storage/anthropic/src/openai/stream_response.rs index 283bc4c798..ac2b28cca9 100644 --- a/rust/cloud-storage/anthropic/src/openai/stream_response.rs +++ b/rust/cloud-storage/anthropic/src/openai/stream_response.rs @@ -3,6 +3,7 @@ use super::stream_extension::{ExtendedAnthropicStreamItem, ExtendedStream}; use crate::client::chat::MessageCompletionResponseStream; use crate::error::AnthropicError; use crate::openai::stream_extension::AnthropicResponseExtension; +use crate::prelude::ServerToolUse; use crate::types::response::{ContentDeltaEvent, StopReason, StreamEvent, Usage}; use crate::{client::chat::Chat, prelude::CreateMessageRequestBody}; use async_openai::error::{ApiError, OpenAIError}; @@ -108,17 +109,51 @@ fn map_stop_reason(stop_reason: StopReason) -> FinishReason { } } +struct PartialTool { + pub name: String, + pub id: String, + pub input: String, +} + +impl TryFrom for ServerToolUse { + type Error = OpenAIError; + fn try_from(value: PartialTool) -> Result { + let any = serde_json::from_str::(&value.input) + .map_err(|e| OpenAIError::JSONDeserialize(e))?; + Ok(Self { + id: value.id, + name: value.name, + input: any, + }) + } +} + +impl From for PartialTool { + fn from(value: ServerToolUse) -> Self { + Self { + id: value.id, + name: value.name, + input: "".into(), + } + } +} + +enum ToolState { + Streaming, + StreamingTool { name: String, id: String }, + StreamingServerTool(PartialTool), +} + fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedStream { Box::pin(stream! { let mut message_id: Option = None; let mut model: Option = None; let created = chrono::Utc::now().timestamp(); - let mut streaming_tool_name = String::new(); - let mut streaming_tool_id = String::new(); + let mut tool_state = ToolState::Streaming; while let Some(part) = stream.next().await { - let result = if let Err(e) = part { - Err(match e { + if let Err(e) = part { + yield Err(match e { AnthropicError::JsonDeserialize(e) => OpenAIError::JSONDeserialize(e), AnthropicError::Reqwest(e) => OpenAIError::Reqwest(e), AnthropicError::StreamError(e) => OpenAIError::StreamError(e), @@ -130,15 +165,13 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS code: Some(status_code.to_string()) }) } - }) } else { match part.unwrap() { StreamEvent::MessageStart { message } => { message_id = message.id.clone(); model = message.model.clone(); - - Ok(create_response( + yield Ok(create_response( &message_id.clone().unwrap_or_default(), &model.clone().unwrap_or_default(), created as u32, @@ -146,12 +179,35 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS create_role_delta(Role::Assistant), None, None, - ).into()) + ).into()); } - StreamEvent::ContentBlockStart { content_block, ..} => { - if let ContentDeltaEvent::ToolUse { name, id, .. } = content_block { - streaming_tool_name = name; - streaming_tool_id = id; + StreamEvent::ContentBlockStart { content_block, index } => { + match content_block { + ContentDeltaEvent::ToolUse { name, id, .. } => { + yield Ok(create_response( + &message_id.clone().unwrap_or_default(), + &model.clone().unwrap_or_default(), + created as u32, + index, + create_tool_call_delta( + index, + Some(id.clone()), + Some(ChatCompletionToolType::Function), + Some(name.clone()), + Some(String::new()), + ), + None, + None, + ).into()); + tool_state = ToolState::StreamingTool { name, id }; + } + ContentDeltaEvent::ServerToolUse(server_tool) => { + tool_state = ToolState::StreamingServerTool(server_tool.into()) + } + ContentDeltaEvent::WebSearchToolResult(web_search_response) => { + yield Ok(AnthropicResponseExtension::WebSearchToolResponse(web_search_response).into()); + } + _ => {} } // Skip content block start events continue; @@ -159,15 +215,15 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS StreamEvent::ContentBlockDelta { index, delta } => { match delta { ContentDeltaEvent::CitationsDelta { citation } => { - Ok( + yield Ok( AnthropicResponseExtension::Citation(citation).into() - ) + ); } ContentDeltaEvent::WebSearchToolResult(web_search_response) => { - Ok(AnthropicResponseExtension::WebSearchToolResponse(web_search_response).into()) + yield Ok(AnthropicResponseExtension::WebSearchToolResponse(web_search_response).into()); } ContentDeltaEvent::TextDelta { text } | ContentDeltaEvent::StartTextDelta { text } => { - Ok(create_response( + yield Ok(create_response( &message_id.clone().unwrap_or_default(), &model.clone().unwrap_or_default(), created as u32, @@ -175,11 +231,11 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS create_content_delta(text), None, None, - ).into()) + ).into()); } ContentDeltaEvent::ThinkingDelta { thinking } => { // OpenAI doesn't have thinking blocks, skip or include as content - Ok(create_response( + yield Ok(create_response( &message_id.clone().unwrap_or_default(), &model.clone().unwrap_or_default(), created as u32, @@ -187,62 +243,61 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS create_content_delta(format!("[Thinking] {}", thinking)), None, None, - ).into()) - } - ContentDeltaEvent::ToolUse { id, name, input } => { - // Map to OpenAI tool call - Ok(create_response( - &message_id.clone().unwrap_or_default(), - &model.clone().unwrap_or_default(), - created as u32, - index, - create_tool_call_delta( - index, - Some(id), - Some(ChatCompletionToolType::Function), - Some(name), - Some(input.to_string()), - ), - None, - None, - ).into()) - } - ContentDeltaEvent::ServerToolUse(server_tool_use) => { - Ok(AnthropicResponseExtension::ServerToolUse(server_tool_use).into()) + ).into()); } ContentDeltaEvent::InputJsonDelta { partial_json } => { - // Stream partial JSON for tool call arguments - Ok(create_response( - &message_id.clone().unwrap_or_default(), - &model.clone().unwrap_or_default(), - created as u32, - index, - create_tool_call_delta( - index, - Some(streaming_tool_id.clone()), - None, - Some(streaming_tool_name.clone()), - Some(partial_json), - ), - None, - None, - ).into()) + match &mut tool_state { + ToolState::Streaming => {}, + ToolState::StreamingTool {name, id} => { + yield Ok(create_response( + &message_id.clone().unwrap_or_default(), + &model.clone().unwrap_or_default(), + created as u32, + index, + create_tool_call_delta( + index, + Some(id.clone()), + None, + Some(name.clone()), + Some(partial_json), + ), + None, + None, + ).into()); + } + ToolState::StreamingServerTool(server_tool) => { + server_tool.input.push_str(partial_json.as_str()); + } + } } - - ContentDeltaEvent::SignatureDelta { .. } => { - // Skip signature deltas as OpenAI doesn't have an equivalent + // signature events are uneeded + unsupported + ContentDeltaEvent::SignatureDelta{..} + // server tool deltas are emitted in content block start events + | ContentDeltaEvent::ServerToolUse(_) + // tool use deltas are emitted in content block start events + | ContentDeltaEvent::ToolUse { .. } + => { continue; } } } StreamEvent::ContentBlockStop { .. } => { - // Skip content block stop events + match tool_state { + ToolState::StreamingServerTool(partial_tool) => { + yield ServerToolUse::try_from(partial_tool) + .map(|tool| { + AnthropicResponseExtension::ServerToolUse(tool).into() + }) + } + _ => {} + } + tool_state = ToolState::Streaming; continue; } StreamEvent::MessageDelta { delta , usage } => { let finish_reason = delta.stop_reason.map(map_stop_reason); - Ok(create_response( + yield Ok(create_response( &message_id.clone().unwrap_or_default(), &model.clone().unwrap_or_default(), created as u32, @@ -250,10 +305,10 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS create_empty_delta(), finish_reason, usage.map(Into::into), - ).into()) + ).into()); } StreamEvent::MessageStop => { - Ok(create_response( + yield Ok(create_response( &message_id.clone().unwrap_or_default(), &model.clone().unwrap_or_default(), created as u32, @@ -261,23 +316,22 @@ fn map_stream_extended(mut stream: MessageCompletionResponseStream) -> ExtendedS create_empty_delta(), Some(FinishReason::Stop), None, - ).into()) + ).into()); } StreamEvent::Ping => { // Skip ping events continue; } StreamEvent::Error { error } => { - Err(OpenAIError::ApiError(ApiError { + yield Err(OpenAIError::ApiError(ApiError { message: format!("{:?}", error), r#type: None, param: None, code: None, - })) + })); } } }; - yield result; } }) } diff --git a/rust/cloud-storage/anthropic/src/types/request/request.rs b/rust/cloud-storage/anthropic/src/types/request/request.rs index 0c275463f3..0ede65e4be 100644 --- a/rust/cloud-storage/anthropic/src/types/request/request.rs +++ b/rust/cloud-storage/anthropic/src/types/request/request.rs @@ -139,6 +139,8 @@ pub enum ToolChoice { None, } +/// Two kinds of tools supported by anthropic +/// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(untagged)] pub enum Tool { diff --git a/rust/cloud-storage/anthropic/src/types/response/citation.rs b/rust/cloud-storage/anthropic/src/types/response/citation.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/rust/cloud-storage/anthropic/src/types/response/mod.rs b/rust/cloud-storage/anthropic/src/types/response/mod.rs index 5d99968f60..c3188b71a1 100644 --- a/rust/cloud-storage/anthropic/src/types/response/mod.rs +++ b/rust/cloud-storage/anthropic/src/types/response/mod.rs @@ -1,7 +1,6 @@ mod stream_types; mod types; -mod web_search; +pub mod web_search; pub use stream_types::*; pub use types::*; -pub use web_search::*; diff --git a/rust/cloud-storage/anthropic/src/types/response/web_search.rs b/rust/cloud-storage/anthropic/src/types/response/web_search.rs index c3f2214b19..00cabe04c5 100644 --- a/rust/cloud-storage/anthropic/src/types/response/web_search.rs +++ b/rust/cloud-storage/anthropic/src/types/response/web_search.rs @@ -1,150 +1,25 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Web search response content returned by Claude when using the web_search tool -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct WebSearchResponse { /// The search query that was executed /// Array of search results pub content: Vec, + pub tool_use_id: String, } /// A single search result from web search -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum SearchResult { WebSearchResult { title: String, url: String }, } -/// Server response notification for web search tool use -/// This appears in the response stream before the actual search results -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -pub struct WebSearchNotification { - /// Unique identifier for this tool use - pub id: String, - /// The name of the tool (should be "web_search") - pub name: String, - /// Input parameters to the web search tool - pub input: WebSearchInput, -} - -/// Input parameters for web search -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -pub struct WebSearchInput { - /// The search query +/// This is the expected shape of the streamed json following a `server_tool_use` in content_block_start event +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +pub struct WebSearchToolCall { pub query: String, - /// Optional list of domains to include results from - #[serde(skip_serializing_if = "Option::is_none")] - pub allowed_domains: Option>, - /// Optional list of domains to exclude from results - #[serde(skip_serializing_if = "Option::is_none")] - pub blocked_domains: Option>, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_web_search_response_serialization() { - let response = WebSearchResponse { - content: vec![ - SearchResult::WebSearchResult { - title: "The Rust Programming Language".to_string(), - url: "https://www.rust-lang.org/".to_string(), - }, - SearchResult::WebSearchResult { - title: "Rust Documentation".to_string(), - url: "https://doc.rust-lang.org/".to_string(), - }, - ], - }; - - let serialized = serde_json::to_value(&response).expect("serialization should succeed"); - let expected = json!({ - "content": [ - { - "type": "Result", - "title": "The Rust Programming Language", - "url": "https://www.rust-lang.org/" - }, - { - "type": "Result", - "title": "Rust Documentation", - "url": "https://doc.rust-lang.org/" - } - ] - }); - - assert_eq!(serialized, expected); - } - - #[test] - fn test_web_search_response_deserialization() { - let json_data = json!({ - "content": [ - { - "type": "Result", - "title": "Async Programming in Rust", - "url": "https://rust-lang.github.io/async-book/" - } - ] - }); - - let response: WebSearchResponse = - serde_json::from_value(json_data).expect("deserialization should succeed"); - - assert_eq!(response.content.len(), 1); - match &response.content[0] { - SearchResult::WebSearchResult { title, url } => { - assert_eq!(title, "Async Programming in Rust"); - assert_eq!(url, "https://rust-lang.github.io/async-book/"); - } - } - } - - #[test] - fn test_web_search_notification() { - let notification = WebSearchNotification { - id: "tool_123".to_string(), - name: "web_search".to_string(), - input: WebSearchInput { - query: "claude api documentation".to_string(), - allowed_domains: Some(vec!["anthropic.com".to_string()]), - blocked_domains: None, - }, - }; - - let serialized = serde_json::to_value(¬ification).expect("serialization should succeed"); - let expected = json!({ - "id": "tool_123", - "name": "web_search", - "input": { - "query": "claude api documentation", - "allowed_domains": ["anthropic.com"] - } - }); - - assert_eq!(serialized, expected); - } - - #[test] - fn test_web_search_input_with_blocked_domains() { - let input = WebSearchInput { - query: "programming tutorials".to_string(), - allowed_domains: None, - blocked_domains: Some(vec!["spam.com".to_string(), "ads.com".to_string()]), - }; - - let serialized = serde_json::to_value(&input).expect("serialization should succeed"); - let expected = json!({ - "query": "programming tutorials", - "blocked_domains": ["spam.com", "ads.com"] - }); - - assert_eq!(serialized, expected); - } } diff --git a/rust/cloud-storage/document_cognition_service/schemas/tools.json b/rust/cloud-storage/document_cognition_service/schemas/tools.json index 1865004efa..6005a5ff35 100644 --- a/rust/cloud-storage/document_cognition_service/schemas/tools.json +++ b/rust/cloud-storage/document_cognition_service/schemas/tools.json @@ -1,645 +1,772 @@ { "schemas": [ { - "name": "UnifiedSearch", + "name": "Read", "inputSchema": { "additionalProperties": false, - "description": "Universal search across all content types (documents, emails, AI conversations, chat/slack threads/channels, projects aka folders). This tool will return broad metadata from successful results and/or content matches. Use the Read tool next to read the results from those matches. Only ever refer to documents by name or with a document mention. Never state the id of the document in plaintext. User's are presented with the results of this tool as a UI element so there is no need to enumerate the information found from this tool.", + "description": "Read content by ID(s). Supports reading documents, channels, chats, and emails by their respective IDs. Use this tool when you need to retrieve the full content of a specific item(s).\n Channel transcripts only include 300 messages. Use 'messages_since' to see messages in a different time window.", "properties": { - "exhaustiveSearch": { - "additionalProperties": false, - "default": true, - "description": "Exhaustive search across all results matching the query. Defaults to true. Use false when the user only requires a limitied subset of results to answer the question", - "type": "boolean" - }, - "pageOffset": { + "contentType": { "additionalProperties": false, - "default": 0, - "description": "Page offset. Default is 0. Use a higher offset to page through results intelligently. Set exhaustive_search to true to get all results.", - "format": "int64", - "type": "integer" + "description": "The type of content to read. Choose based on the type of content you want to retrieve.", + "enum": [ + "document", + "channel", + "channel-message", + "chat-thread", + "chat-message", + "email-thread", + "email-message", + "project" + ], + "type": "string" }, - "pageSize": { + "ids": { "additionalProperties": false, - "default": 10, - "description": "Search count per search type (i.e. applies separately to each of documents, emails, ai_conversations, chats, projects). Max is 100. Default is 10. Does not apply when exhaustive_search is set to true.", - "format": "int64", - "type": "integer" + "description": "ID(s) of the content to read. IMPORTANT: document, channel-message, chat-message, and email-message content types support MULTIPLE ids! For all other content types (channel, chat-thread, email-thread) provide a single id.", + "items": { + "additionalProperties": false, + "type": "string" + }, + "type": "array" }, - "request": { + "messagesSince": { "additionalProperties": false, - "description": "Aggregated search request, see individual fields for details", - "properties": { - "filters": { - "additionalProperties": false, - "description": "Search filters for various kinds of items. Set the entire filters property as `null` if you do not have specific filters for a given type, e.g. bcc for email filters.", + "description": "A local datetime of the earliest message to include in a channel transcript ex: 2025-11-25 12:00:09 EST, only applicable to channels", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "contentType", + "ids", + "messagesSince" + ], + "title": "Read", + "type": "object" + }, + "outputSchema": { + "properties": { + "content": { + "oneOf": [ + { "properties": { - "channel": { - "additionalProperties": false, - "description": "Channel filters. `null` to not filter channels searched over.", - "properties": { - "channel_ids": { - "additionalProperties": false, - "description": "Channel IDs to search within. Examples: ['general']. Empty to search all accessible channels.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "mentions": { - "additionalProperties": false, - "description": "Channel user mentions to search for. Examples: ['@username']. Empty if not filtering by mentions.", - "items": { - "additionalProperties": false, + "documents": { + "items": { + "properties": { + "content": { "type": "string" }, - "type": "array" - }, - "org_id": { - "additionalProperties": false, - "description": "Channel organization ID to search within. Empty to ignore organization filtering.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "sender_ids": { - "additionalProperties": false, - "description": "Sender IDs to search within. Examples: ['user1']. Empty to search all accessible senders.", - "items": { - "additionalProperties": false, + "documentId": { "type": "string" }, - "type": "array" + "metadata": { + "properties": { + "deleted": { + "type": "boolean" + }, + "documentName": { + "type": "string" + }, + "fileType": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": "string" + }, + "projectId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "documentName", + "owner", + "deleted" + ], + "type": "object" + } }, - "thread_ids": { - "additionalProperties": false, - "description": "Channel thread IDs to search within. Examples: ['thread123']. Empty to search all threads.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - } + "required": [ + "documentId", + "content", + "metadata" + ], + "type": "object" }, - "required": [ - "channel_ids", - "mentions", - "org_id", - "sender_ids", - "thread_ids" - ], + "type": "array" + }, + "type": { + "const": "documents", + "type": "string" + } + }, + "required": [ + "type", + "documents" + ], + "type": "object" + }, + { + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { "type": [ - "object", + "string", "null" ] }, - "chat": { - "additionalProperties": false, - "description": "Chat filters. `null` to not filter chats searched over.", - "properties": { - "chat_ids": { - "additionalProperties": false, - "description": "Chat ids to search over. Examples: ['chat1'], ['chat1', 'chat2']. When provided, chat search will only match results on these chats. Empty to search all accessible chats.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "owners": { - "additionalProperties": false, - "description": "Filter by chat owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "project_ids": { - "additionalProperties": false, - "description": "A list of project ids to search within. Examples: ['project1']. Empty to ignore project filtering.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "role": { - "additionalProperties": false, - "description": "Chat message roles to search. Examples: ['user'], ['assistant']. Empty to search all roles.", - "items": { - "additionalProperties": false, + "transcript": { + "type": "string" + }, + "type": { + "const": "channel", + "type": "string" + } + }, + "required": [ + "type", + "channel_id", + "transcript" + ], + "type": "object" + }, + { + "properties": { + "conversation": { + "items": { + "properties": { + "chat_id": { "type": "string" }, - "type": "array" - } - }, - "required": [ - "chat_ids", - "owners", - "project_ids", - "role" - ], - "type": [ - "object", - "null" - ] - }, - "document": { - "additionalProperties": false, - "description": "Document filters. `null` to not filter documents searched over.", - "properties": { - "document_ids": { - "additionalProperties": false, - "description": "Document ids to search over. Examples: ['doc1'], ['doc1', 'doc2']. Empty to search all accessible documents.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "file_types": { - "additionalProperties": false, - "description": "Document file types to search. Examples: ['pdf'], ['md', 'txt']. Empty to search all file types.", - "items": { - "additionalProperties": false, - "type": "string" + "messages": { + "items": { + "properties": { + "attachment_summaries": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "Summary": { + "properties": { + "created_at": { + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "document_id": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "summary": { + "type": "string" + }, + "version_id": { + "type": "string" + } + }, + "required": [ + "document_id", + "summary", + "version_id" + ], + "type": "object" + } + }, + "required": [ + "Summary" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "NoSummary": { + "properties": { + "document_id": { + "type": "string" + } + }, + "required": [ + "document_id" + ], + "type": "object" + } + }, + "required": [ + "NoSummary" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "content": { + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "content", + "date", + "attachment_summaries" + ], + "type": "object" + }, + "type": "array" }, - "type": "array" - }, - "owners": { - "additionalProperties": false, - "description": "Filter by document owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", - "items": { - "additionalProperties": false, + "title": { "type": "string" - }, - "type": "array" + } }, - "project_ids": { - "additionalProperties": false, - "description": "A list of project ids to search within. Examples: ['project1'].\nfiltering. Empty to ignore project filtering.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - } + "required": [ + "chat_id", + "title", + "messages" + ], + "type": "object" }, - "required": [ - "document_ids", - "file_types", - "owners", - "project_ids" - ], - "type": [ - "object", - "null" - ] + "type": "array" }, - "email": { - "additionalProperties": false, - "description": "Email filters. `null` to not filter emails searched over.", - "properties": { - "bcc": { - "additionalProperties": false, - "description": "Email BCC addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by BCC.", - "items": { - "additionalProperties": false, - "type": "string" + "type": { + "const": "chat", + "type": "string" + } + }, + "required": [ + "type", + "conversation" + ], + "type": "object" + }, + { + "properties": { + "messages": { + "items": { + "properties": { + "bcc": { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - "cc": { - "additionalProperties": false, - "description": "Email CC addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by CC.", - "items": { - "additionalProperties": false, + "cc": { + "items": { + "type": "string" + }, + "type": "array" + }, + "content": { "type": "string" }, - "type": "array" - }, - "recipients": { - "additionalProperties": false, - "description": "Email Recipient addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by Recipient.", - "items": { - "additionalProperties": false, + "messageId": { "type": "string" }, - "type": "array" - }, - "senders": { - "additionalProperties": false, - "description": "Email sender addresses to filter by. Examples: ['user@example.com']. Empty to search all senders.", - "items": { - "additionalProperties": false, + "recipients": { + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { "type": "string" }, - "type": "array" - } + "sentAt": { + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "messageId", + "sender", + "recipients", + "cc", + "bcc", + "content" + ], + "type": "object" }, - "required": [ - "bcc", - "cc", - "recipients", - "senders" - ], + "type": "array" + }, + "subject": { "type": [ - "object", + "string", "null" ] }, - "project": { - "additionalProperties": false, - "description": "Project filters. `null` to not filter projects searched over.", - "properties": { - "owners": { - "additionalProperties": false, - "description": "Filter by project owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - }, - "project_ids": { - "additionalProperties": false, - "description": "Project IDs to search within. Examples: ['project1']. Empty to search all accessible projects.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "owners", - "project_ids" - ], - "type": [ - "object", - "null" - ] + "thread_id": { + "type": "string" + }, + "type": { + "const": "email", + "type": "string" } }, "required": [ - "channel", - "chat", - "document", - "email", - "project" + "type", + "thread_id", + "messages" ], - "type": [ - "object", - "null" - ] + "type": "object" }, - "include": { - "additionalProperties": false, - "default": [], - "description": "Include specific entity types from search. If empty, all entity types will be searched over. If you are unsure which types to search, use an empty array to search all.", - "items": { - "additionalProperties": false, - "enum": [ - "documents", - "chats", - "emails", - "channels", - "projects" - ], - "type": "string" + { + "properties": { + "formatted_preview": { + "type": "string" + }, + "type": { + "const": "itemPreviews", + "type": "string" + } }, - "type": "array" - }, - "match_type": { - "additionalProperties": false, - "description": "How to match the search terms. 'exact' for precise case-sensitive phrase matches, 'partial' for prefix/partial matches. REQUIRED field.", - "enum": [ - "exact", - "partial", - "regexp" - ], - "type": "string" - }, - "search_on": { - "additionalProperties": false, - "default": "content", - "description": "Fields to search on (Name, Content, NameContent). Defaults to Content", - "enum": [ - "name", - "content", - "name_content" + "required": [ + "type", + "formatted_preview" ], - "type": "string" - }, - "terms": { - "additionalProperties": false, - "description": "Multiple distinct search terms as separate strings. Use this for keyword-based searches where you want to find content containing any of these terms. Each term must be at least 3 characters (shorter terms are automatically filtered out). Examples: ['machine', 'learning', 'algorithms'], ['project', 'status', 'update']. `null` this field if searching without text terms to search all. This field matches query string against both name and content.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": [ - "array", - "null" - ] + "type": "object" } - }, - "required": [ - "filters", - "include", - "match_type", - "search_on", - "terms" - ], - "type": "object" + ] } }, "required": [ - "exhaustiveSearch", - "pageOffset", - "pageSize", - "request" + "content" ], - "title": "UnifiedSearch", + "title": "ReadResponse", "type": "object" - }, - "outputSchema": { + } + }, + { + "name": "UnifiedSearch", + "inputSchema": { + "additionalProperties": false, + "description": "Universal search across all content types (documents, emails, AI conversations, chat/slack threads/channels, projects aka folders). This tool will return broad metadata from successful results and/or content matches. Use the Read tool next to read the results from those matches. Only ever refer to documents by name or with a document mention. Never state the id of the document in plaintext. User's are presented with the results of this tool as a UI element so there is no need to enumerate the information found from this tool.", "properties": { - "response": { - "description": "The search results", + "exhaustiveSearch": { + "additionalProperties": false, + "default": true, + "description": "Exhaustive search across all results matching the query. Defaults to true. Use false when the user only requires a limitied subset of results to answer the question", + "type": "boolean" + }, + "pageOffset": { + "additionalProperties": false, + "default": 0, + "description": "Page offset. Default is 0. Use a higher offset to page through results intelligently. Set exhaustive_search to true to get all results.", + "format": "int64", + "type": "integer" + }, + "pageSize": { + "additionalProperties": false, + "default": 10, + "description": "Search count per search type (i.e. applies separately to each of documents, emails, ai_conversations, chats, projects). Max is 100. Default is 10. Does not apply when exhaustive_search is set to true.", + "format": "int64", + "type": "integer" + }, + "request": { + "additionalProperties": false, + "description": "Aggregated search request, see individual fields for details", "properties": { - "results": { - "description": "The search results (truncated to `results_returned` limit if applicable)", - "items": { - "oneOf": [ - { - "properties": { - "document_id": { - "description": "The document id", + "filters": { + "additionalProperties": false, + "description": "Search filters for various kinds of items. Set the entire filters property as `null` if you do not have specific filters for a given type, e.g. bcc for email filters.", + "properties": { + "channel": { + "additionalProperties": false, + "description": "Channel filters. `null` to not filter channels searched over.", + "properties": { + "channel_ids": { + "additionalProperties": false, + "description": "Channel IDs to search within. Examples: ['general']. Empty to search all accessible channels.", + "items": { + "additionalProperties": false, "type": "string" }, - "document_name": { - "description": "The document name", + "type": "array" + }, + "mentions": { + "additionalProperties": false, + "description": "Channel user mentions to search for. Examples: ['@username']. Empty if not filtering by mentions.", + "items": { + "additionalProperties": false, "type": "string" }, - "file_type": { - "description": "The file type", + "type": "array" + }, + "org_id": { + "additionalProperties": false, + "description": "Channel organization ID to search within. Empty to ignore organization filtering.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "sender_ids": { + "additionalProperties": false, + "description": "Sender IDs to search within. Examples: ['user1']. Empty to search all accessible senders.", + "items": { + "additionalProperties": false, "type": "string" }, - "highlight": { - "description": "The highlights on the document", - "properties": { - "content": { - "description": "The highlight match on the content field", - "items": { - "type": "string" - }, - "type": "array" - }, - "name": { - "description": "The highlight match on the name field", - "type": [ - "string", - "null" - ] - } - }, - "type": "object" + "type": "array" + }, + "thread_ids": { + "additionalProperties": false, + "description": "Channel thread IDs to search within. Examples: ['thread123']. Empty to search all threads.", + "items": { + "additionalProperties": false, + "type": "string" }, - "node_id": { - "description": "The node id", + "type": "array" + } + }, + "required": [ + "channel_ids", + "mentions", + "org_id", + "sender_ids", + "thread_ids" + ], + "type": [ + "object", + "null" + ] + }, + "chat": { + "additionalProperties": false, + "description": "Chat filters. `null` to not filter chats searched over.", + "properties": { + "chat_ids": { + "additionalProperties": false, + "description": "Chat ids to search over. Examples: ['chat1'], ['chat1', 'chat2']. When provided, chat search will only match results on these chats. Empty to search all accessible chats.", + "items": { + "additionalProperties": false, "type": "string" }, - "owner_id": { - "description": "The owner id", + "type": "array" + }, + "owners": { + "additionalProperties": false, + "description": "Filter by chat owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", + "items": { + "additionalProperties": false, "type": "string" }, - "raw_content": { - "description": "The raw content of the document", - "type": [ - "string", - "null" - ] - }, - "type": { - "const": "document", + "type": "array" + }, + "project_ids": { + "additionalProperties": false, + "description": "A list of project ids to search within. Examples: ['project1']. Empty to ignore project filtering.", + "items": { + "additionalProperties": false, "type": "string" }, - "updated_at": { - "description": "The time the document was last updated", - "format": "date-time", - "type": "string" - } + "type": "array" }, - "required": [ - "type", - "document_id", - "document_name", - "node_id", - "owner_id", - "file_type", - "updated_at", - "highlight" - ], - "type": "object" - }, - { - "properties": { - "chat_id": { - "description": "The chat id", + "role": { + "additionalProperties": false, + "description": "Chat message roles to search. Examples: ['user'], ['assistant']. Empty to search all roles.", + "items": { + "additionalProperties": false, "type": "string" }, - "chat_message_id": { - "description": "The chat message id", + "type": "array" + } + }, + "required": [ + "chat_ids", + "owners", + "project_ids", + "role" + ], + "type": [ + "object", + "null" + ] + }, + "document": { + "additionalProperties": false, + "description": "Document filters. `null` to not filter documents searched over.", + "properties": { + "document_ids": { + "additionalProperties": false, + "description": "Document ids to search over. Examples: ['doc1'], ['doc1', 'doc2']. Empty to search all accessible documents.", + "items": { + "additionalProperties": false, "type": "string" }, - "highlight": { - "description": "The highlights on the chat", - "properties": { - "content": { - "description": "The highlight match on the content field", - "items": { - "type": "string" - }, - "type": "array" - }, - "name": { - "description": "The highlight match on the name field", - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "role": { - "description": "The role", + "type": "array" + }, + "file_types": { + "additionalProperties": false, + "description": "Document file types to search. Examples: ['pdf'], ['md', 'txt']. Empty to search all file types.", + "items": { + "additionalProperties": false, "type": "string" }, - "title": { - "description": "The title", + "type": "array" + }, + "owners": { + "additionalProperties": false, + "description": "Filter by document owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", + "items": { + "additionalProperties": false, "type": "string" }, - "type": { - "const": "chat", + "type": "array" + }, + "project_ids": { + "additionalProperties": false, + "description": "A list of project ids to search within. Examples: ['project1'].\nfiltering. Empty to ignore project filtering.", + "items": { + "additionalProperties": false, "type": "string" }, - "updated_at": { - "description": "The time the chat was last updated", - "format": "date-time", + "type": "array" + } + }, + "required": [ + "document_ids", + "file_types", + "owners", + "project_ids" + ], + "type": [ + "object", + "null" + ] + }, + "email": { + "additionalProperties": false, + "description": "Email filters. `null` to not filter emails searched over.", + "properties": { + "bcc": { + "additionalProperties": false, + "description": "Email BCC addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by BCC.", + "items": { + "additionalProperties": false, "type": "string" }, - "user_id": { - "description": "The user id", - "type": "string" - } + "type": "array" }, - "required": [ - "type", - "chat_id", - "chat_message_id", - "user_id", - "role", - "updated_at", - "title", - "highlight" - ], - "type": "object" - }, - { - "properties": { - "bcc": { - "description": "The bcc", - "items": { - "type": "string" - }, - "type": "array" - }, - "cc": { - "description": "The cc", - "items": { - "type": "string" - }, - "type": "array" - }, - "highlight": { - "description": "The highlights on the email", - "properties": { - "content": { - "description": "The highlight match on the content field", - "items": { - "type": "string" - }, - "type": "array" - }, - "name": { - "description": "The highlight match on the name field", - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "labels": { - "description": "The labels", - "items": { - "type": "string" - }, - "type": "array" - }, - "link_id": { - "description": "The link id", + "cc": { + "additionalProperties": false, + "description": "Email CC addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by CC.", + "items": { + "additionalProperties": false, "type": "string" }, - "message_id": { - "description": "The message id", + "type": "array" + }, + "recipients": { + "additionalProperties": false, + "description": "Email Recipient addresses to filter by. Examples: ['user@example.com']. Empty if not filtering by Recipient.", + "items": { + "additionalProperties": false, "type": "string" }, - "recipients": { - "description": "The recipients", - "items": { - "type": "string" - }, - "type": "array" + "type": "array" + }, + "senders": { + "additionalProperties": false, + "description": "Email sender addresses to filter by. Examples: ['user@example.com']. Empty to search all senders.", + "items": { + "additionalProperties": false, + "type": "string" }, - "sender": { - "description": "The sender", + "type": "array" + } + }, + "required": [ + "bcc", + "cc", + "recipients", + "senders" + ], + "type": [ + "object", + "null" + ] + }, + "project": { + "additionalProperties": false, + "description": "Project filters. `null` to not filter projects searched over.", + "properties": { + "owners": { + "additionalProperties": false, + "description": "Filter by project owner. Examples: ['macro|user1@user.com'], ['macro|user1@user.com', 'macro|user2@user.com']. Empty to search all owners.", + "items": { + "additionalProperties": false, "type": "string" }, - "sent_at": { - "description": "The time the email was sent", - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "subject": { - "description": "The subject", - "type": [ - "string", - "null" - ] - }, - "thread_id": { - "description": "The thread id", - "type": "string" - }, - "type": { - "const": "email", - "type": "string" - }, - "updated_at": { - "description": "The time the email was last updated", - "format": "date-time", + "type": "array" + }, + "project_ids": { + "additionalProperties": false, + "description": "Project IDs to search within. Examples: ['project1']. Empty to search all accessible projects.", + "items": { + "additionalProperties": false, "type": "string" }, - "user_id": { - "description": "The user id", - "type": "string" - } - }, - "required": [ - "type", - "thread_id", - "message_id", - "sender", - "recipients", - "cc", - "bcc", - "labels", - "link_id", - "user_id", - "updated_at", - "highlight" - ], - "type": "object" + "type": "array" + } }, + "required": [ + "owners", + "project_ids" + ], + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "channel", + "chat", + "document", + "email", + "project" + ], + "type": [ + "object", + "null" + ] + }, + "include": { + "additionalProperties": false, + "default": [], + "description": "Include specific entity types from search. If empty, all entity types will be searched over. If you are unsure which types to search, use an empty array to search all.", + "items": { + "additionalProperties": false, + "enum": [ + "documents", + "chats", + "emails", + "channels", + "projects" + ], + "type": "string" + }, + "type": "array" + }, + "match_type": { + "additionalProperties": false, + "description": "How to match the search terms. 'exact' for precise case-sensitive phrase matches, 'partial' for prefix/partial matches. REQUIRED field.", + "enum": [ + "exact", + "partial", + "regexp" + ], + "type": "string" + }, + "search_on": { + "additionalProperties": false, + "default": "content", + "description": "Fields to search on (Name, Content, NameContent). Defaults to Content", + "enum": [ + "name", + "content", + "name_content" + ], + "type": "string" + }, + "terms": { + "additionalProperties": false, + "description": "Multiple distinct search terms as separate strings. Use this for keyword-based searches where you want to find content containing any of these terms. Each term must be at least 3 characters (shorter terms are automatically filtered out). Examples: ['machine', 'learning', 'algorithms'], ['project', 'status', 'update']. `null` this field if searching without text terms to search all. This field matches query string against both name and content.", + "items": { + "additionalProperties": false, + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "filters", + "include", + "match_type", + "search_on", + "terms" + ], + "type": "object" + } + }, + "required": [ + "exhaustiveSearch", + "pageOffset", + "pageSize", + "request" + ], + "title": "UnifiedSearch", + "type": "object" + }, + "outputSchema": { + "properties": { + "response": { + "description": "The search results", + "properties": { + "results": { + "description": "The search results (truncated to `results_returned` limit if applicable)", + "items": { + "oneOf": [ { "properties": { - "channel_id": { - "description": "The channel id", + "document_id": { + "description": "The document id", "type": "string" }, - "channel_type": { - "description": "The channel type", + "document_name": { + "description": "The document name", "type": "string" }, - "created_at": { - "description": "The time the channel message was created", - "format": "date-time", + "file_type": { + "description": "The file type", "type": "string" }, "highlight": { - "description": "The highlights on the channel message", + "description": "The highlights on the document", "properties": { + "bcc": { + "description": "The highlight match on the bcc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The highlight match on the cc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, "content": { "description": "The highlight match on the content field", "items": { @@ -653,58 +780,63 @@ "string", "null" ] + }, + "recipients": { + "description": "The highlight match on the recipients (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The highlight match on the sender (email only)", + "type": [ + "string", + "null" + ] + }, + "user_id": { + "description": "The highlight match on the user (owner) of the entity", + "type": [ + "string", + "null" + ] } }, "type": "object" }, - "mentions": { - "description": "The mentions", - "items": { - "type": "string" - }, - "type": "array" - }, - "message_id": { - "description": "The message id", + "node_id": { + "description": "The node id", "type": "string" }, - "org_id": { - "description": "The org id", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "sender_id": { - "description": "The sender id", + "owner_id": { + "description": "The owner id", "type": "string" }, - "thread_id": { - "description": "The thread id", + "raw_content": { + "description": "The raw content of the document", "type": [ "string", "null" ] }, "type": { - "const": "channel", + "const": "document", "type": "string" }, "updated_at": { - "description": "The time the channel message was last updated", + "description": "The time the document was last updated", "format": "date-time", "type": "string" } }, "required": [ "type", - "channel_id", - "channel_type", - "message_id", - "sender_id", - "mentions", - "created_at", + "document_id", + "document_name", + "node_id", + "owner_id", + "file_type", "updated_at", "highlight" ], @@ -712,14 +844,31 @@ }, { "properties": { - "created_at": { - "description": "The time the project was created", - "format": "date-time", + "chat_id": { + "description": "The chat id", + "type": "string" + }, + "chat_message_id": { + "description": "The chat message id", "type": "string" }, "highlight": { - "description": "The highlights on the project", + "description": "The highlights on the chat", "properties": { + "bcc": { + "description": "The highlight match on the bcc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The highlight match on the cc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, "content": { "description": "The highlight match on the content field", "items": { @@ -733,285 +882,462 @@ "string", "null" ] - } + }, + "recipients": { + "description": "The highlight match on the recipients (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The highlight match on the sender (email only)", + "type": [ + "string", + "null" + ] + }, + "user_id": { + "description": "The highlight match on the user (owner) of the entity", + "type": [ + "string", + "null" + ] + } }, "type": "object" }, - "project_id": { - "description": "The project id", + "role": { + "description": "The role", "type": "string" }, - "project_name": { - "description": "The project name", + "title": { + "description": "The title", "type": "string" }, "type": { - "const": "project", + "const": "chat", "type": "string" }, "updated_at": { - "description": "The time the project was last updated", + "description": "The time the chat was last updated", "format": "date-time", "type": "string" }, "user_id": { - "description": "The id of the user who created the project", + "description": "The user id", "type": "string" } }, "required": [ "type", - "project_id", + "chat_id", + "chat_message_id", + "user_id", + "role", "updated_at", - "created_at", - "project_name", + "title", + "highlight" + ], + "type": "object" + }, + { + "properties": { + "bcc": { + "description": "The bcc", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The cc", + "items": { + "type": "string" + }, + "type": "array" + }, + "highlight": { + "description": "The highlights on the email", + "properties": { + "bcc": { + "description": "The highlight match on the bcc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The highlight match on the cc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "content": { + "description": "The highlight match on the content field", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The highlight match on the name field", + "type": [ + "string", + "null" + ] + }, + "recipients": { + "description": "The highlight match on the recipients (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The highlight match on the sender (email only)", + "type": [ + "string", + "null" + ] + }, + "user_id": { + "description": "The highlight match on the user (owner) of the entity", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "labels": { + "description": "The labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "link_id": { + "description": "The link id", + "type": "string" + }, + "message_id": { + "description": "The message id", + "type": "string" + }, + "recipients": { + "description": "The recipients", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The sender", + "type": "string" + }, + "sent_at": { + "description": "The time the email was sent", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "subject": { + "description": "The subject", + "type": [ + "string", + "null" + ] + }, + "thread_id": { + "description": "The thread id", + "type": "string" + }, + "type": { + "const": "email", + "type": "string" + }, + "updated_at": { + "description": "The time the email was last updated", + "format": "date-time", + "type": "string" + }, + "user_id": { + "description": "The user id", + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "message_id", + "sender", + "recipients", + "cc", + "bcc", + "labels", + "link_id", "user_id", + "updated_at", "highlight" ], "type": "object" - } - ] - }, - "type": "array" - }, - "resultsReturned": { - "description": "the number of results returned", - "format": "uint", - "minimum": 0, - "type": "integer" - }, - "totalResults": { - "description": "total number of results from search", - "format": "uint", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "results", - "totalResults", - "resultsReturned" - ], - "type": "object" - }, - "responseSchema": { - "description": "The JSON schema for the response so the LLM can understand it" - } - }, - "required": [ - "response", - "responseSchema" - ], - "title": "UnifiedSearchResponseOutput", - "type": "object" - } - }, - { - "name": "WebSearch", - "inputSchema": { - "additionalProperties": false, - "description": "Trigger an intelligent internet search tool with a search query. Phrase the query as a natural language question. The search tool only has the information passed into the query string to include any relevant context from the conversation. Use this tool when the user specifically requests a web search, asks for resources, asks for links, asks you to read documentationWhen referencing links returned by search remember to use github markdown notation to format them like this [description](https://example.com)Our CEO describes when to use web search like this: I think some important criteria for choosing to search the web are\",\n if the user is asking for time-sensitive information (sports scores, news, current happenings, etc) that would have changed since the knowledge cutoff date\n Specific questions that reference external sources, eg contain a hyperlink in the user message or explicitly say “use webmd” or “check arxiv”\n do not use web search for things that are likely to find SEO slop content; this will make it worse than if it didn’t do it\n the LLM is be allowed to guess an answer and then use the web search tool to check AFTER providing the answer, if it feels it’s necessary. E.g. user asks “when was Caesar stabbed” -> llm gives answer -> if needed use web to double check\"\n do not use this tool many times in a single response, you think the user may need more information ask them before calling web search again. You should always sharethe results of your search with the user even if you are not sure if they are useful", - "properties": { - "query": { - "additionalProperties": false, - "description": "The search string to search for. Should be long / descriptive", - "type": "string" - } - }, - "required": [ - "query" - ], - "title": "WebSearch", - "type": "object" - }, - "outputSchema": { - "properties": { - "content": { - "type": "string" - }, - "results": { - "items": { - "properties": { - "name": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "url", - "name" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "content", - "results" - ], - "title": "SearchResults", - "type": "object" - } - }, - { - "name": "ListDocuments", - "inputSchema": { - "additionalProperties": false, - "description": "List documents the user has access to with optional filtering and pagination. Only applies to documents, not emails, AI conversations, chat/slack threads, projects aka folders. This tool returns document metadata including access levels and supports filtering by file type, minimum access level, and pagination. Use this tool to discover and browse documents before using the Read tool to access their content. Prefer using the search tool to search on a specific matching string within the content or the name of the entity.", - "properties": { - "exhaustiveSearch": { - "additionalProperties": false, - "default": false, - "description": "Exhaustive search to get all results. Defaults to false. Set to true when you need all matching documents, ignoring pagination limits.", - "type": "boolean" - }, - "fileTypes": { - "additionalProperties": false, - "description": "Document file types to include. Examples: ['pdf'], ['md', 'txt']. Leave empty to include all document types.", - "items": { - "additionalProperties": false, - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "minAccessLevel": { - "additionalProperties": false, - "description": "Minimum access level required. Defaults to 'view' if not specified.", - "enum": [ - "view", - "comment", - "edit", - "owner", - null - ], - "type": [ - "string", - "null" - ] - }, - "pageOffset": { - "additionalProperties": false, - "default": 0, - "description": "Page offset for pagination. Default is 0. Use higher values to get subsequent pages of results.", - "format": "int64", - "type": "integer" - }, - "pageSize": { - "additionalProperties": false, - "default": 50, - "description": "Number of results per page. Max is 100, default is 50. Use smaller values for focused results.", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "exhaustiveSearch", - "fileTypes", - "minAccessLevel", - "pageOffset", - "pageSize" - ], - "title": "ListDocuments", - "type": "object" - }, - "outputSchema": { - "properties": { - "results": { - "description": "The list results (truncated to `results_returned` limit if applicable)", - "items": { - "properties": { - "access_level": { - "description": "The user's access level to this document", - "enum": [ - "view", - "comment", - "edit", - "owner" - ], - "type": "string" - }, - "created_at": { - "description": "When the document was created", - "format": "date-time", - "type": "string" - }, - "deleted_at": { - "description": "When the document was deleted (null if not deleted)", - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "document_id": { - "description": "The document id", - "type": "string" - }, - "document_name": { - "description": "The document name", - "type": "string" - }, - "file_type": { - "description": "The file type of the document", - "type": [ - "string", - "null" - ] - }, - "owner": { - "description": "The owner of the document", - "type": "string" - }, - "project_id": { - "description": "The project id containing the document", - "type": [ - "string", - "null" + }, + { + "properties": { + "channel_id": { + "description": "The channel id", + "type": "string" + }, + "channel_type": { + "description": "The channel type", + "type": "string" + }, + "created_at": { + "description": "The time the channel message was created", + "format": "date-time", + "type": "string" + }, + "highlight": { + "description": "The highlights on the channel message", + "properties": { + "bcc": { + "description": "The highlight match on the bcc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The highlight match on the cc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "content": { + "description": "The highlight match on the content field", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The highlight match on the name field", + "type": [ + "string", + "null" + ] + }, + "recipients": { + "description": "The highlight match on the recipients (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The highlight match on the sender (email only)", + "type": [ + "string", + "null" + ] + }, + "user_id": { + "description": "The highlight match on the user (owner) of the entity", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "mentions": { + "description": "The mentions", + "items": { + "type": "string" + }, + "type": "array" + }, + "message_id": { + "description": "The message id", + "type": "string" + }, + "org_id": { + "description": "The org id", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "sender_id": { + "description": "The sender id", + "type": "string" + }, + "thread_id": { + "description": "The thread id", + "type": [ + "string", + "null" + ] + }, + "type": { + "const": "channel", + "type": "string" + }, + "updated_at": { + "description": "The time the channel message was last updated", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "type", + "channel_id", + "channel_type", + "message_id", + "sender_id", + "mentions", + "created_at", + "updated_at", + "highlight" + ], + "type": "object" + }, + { + "properties": { + "created_at": { + "description": "The time the project was created", + "format": "date-time", + "type": "string" + }, + "highlight": { + "description": "The highlights on the project", + "properties": { + "bcc": { + "description": "The highlight match on the bcc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "cc": { + "description": "The highlight match on the cc (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "content": { + "description": "The highlight match on the content field", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The highlight match on the name field", + "type": [ + "string", + "null" + ] + }, + "recipients": { + "description": "The highlight match on the recipients (email only)", + "items": { + "type": "string" + }, + "type": "array" + }, + "sender": { + "description": "The highlight match on the sender (email only)", + "type": [ + "string", + "null" + ] + }, + "user_id": { + "description": "The highlight match on the user (owner) of the entity", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "project_id": { + "description": "The project id", + "type": "string" + }, + "project_name": { + "description": "The project name", + "type": "string" + }, + "type": { + "const": "project", + "type": "string" + }, + "updated_at": { + "description": "The time the project was last updated", + "format": "date-time", + "type": "string" + }, + "user_id": { + "description": "The id of the user who created the project", + "type": "string" + } + }, + "required": [ + "type", + "project_id", + "updated_at", + "created_at", + "project_name", + "user_id", + "highlight" + ], + "type": "object" + } ] }, - "updated_at": { - "description": "When the document was last updated", - "format": "date-time", - "type": "string" - } + "type": "array" }, - "required": [ - "document_id", - "document_name", - "owner", - "created_at", - "updated_at", - "access_level" - ], - "type": "object" + "resultsReturned": { + "description": "the number of results returned", + "format": "uint", + "minimum": 0, + "type": "integer" + }, + "totalResults": { + "description": "total number of results from search", + "format": "uint", + "minimum": 0, + "type": "integer" + } }, - "type": "array" - }, - "resultsReturned": { - "description": "The number of results returned", - "format": "uint", - "minimum": 0, - "type": "integer" + "required": [ + "results", + "totalResults", + "resultsReturned" + ], + "type": "object" }, - "totalResults": { - "description": "Total number of results found", - "format": "uint", - "minimum": 0, - "type": "integer" + "responseSchema": { + "description": "The JSON schema for the response so the LLM can understand it" } }, "required": [ - "results", - "totalResults", - "resultsReturned" + "response", + "responseSchema" ], - "title": "ListDocumentsResponse", + "title": "UnifiedSearchResponseOutput", "type": "object" } }, @@ -1410,59 +1736,12 @@ "contacts", "labels", "metadata" - ], - "type": "object" - }, - "type": "array" - }, - "next_cursor": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "items" - ], - "title": "ApiPaginatedThreadCursor", - "type": "object" - } - }, - { - "name": "Read", - "inputSchema": { - "additionalProperties": false, - "description": "Read content by ID(s). Supports reading documents, channels, chats, and emails by their respective IDs. Use this tool when you need to retrieve the full content of a specific item(s).\n Channel transcripts only include 300 messages. Use 'messages_since' to see messages in a different time window.", - "properties": { - "contentType": { - "additionalProperties": false, - "description": "The type of content to read. Choose based on the type of content you want to retrieve.", - "enum": [ - "document", - "channel", - "channel-message", - "chat-thread", - "chat-message", - "email-thread", - "email-message", - "project" - ], - "type": "string" - }, - "ids": { - "additionalProperties": false, - "description": "ID(s) of the content to read. IMPORTANT: document, channel-message, chat-message, and email-message content types support MULTIPLE ids! For all other content types (channel, chat-thread, email-thread) provide a single id.", - "items": { - "additionalProperties": false, - "type": "string" + ], + "type": "object" }, "type": "array" }, - "messagesSince": { - "additionalProperties": false, - "description": "A local datetime of the earliest message to include in a channel transcript ex: 2025-11-25 12:00:09 EST, only applicable to channels", - "format": "date-time", + "next_cursor": { "type": [ "string", "null" @@ -1470,322 +1749,168 @@ } }, "required": [ - "contentType", - "ids", - "messagesSince" + "items" ], - "title": "Read", + "title": "ApiPaginatedThreadCursor", "type": "object" - }, - "outputSchema": { + } + }, + { + "name": "ListDocuments", + "inputSchema": { + "additionalProperties": false, + "description": "List documents the user has access to with optional filtering and pagination. Only applies to documents, not emails, AI conversations, chat/slack threads, projects aka folders. This tool returns document metadata including access levels and supports filtering by file type, minimum access level, and pagination. Use this tool to discover and browse documents before using the Read tool to access their content. Prefer using the search tool to search on a specific matching string within the content or the name of the entity.", "properties": { - "content": { - "oneOf": [ - { - "properties": { - "documents": { - "items": { - "properties": { - "content": { - "type": "string" - }, - "documentId": { - "type": "string" - }, - "metadata": { - "properties": { - "deleted": { - "type": "boolean" - }, - "documentName": { - "type": "string" - }, - "fileType": { - "type": [ - "string", - "null" - ] - }, - "owner": { - "type": "string" - }, - "projectId": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "documentName", - "owner", - "deleted" - ], - "type": "object" - } - }, - "required": [ - "documentId", - "content", - "metadata" - ], - "type": "object" - }, - "type": "array" - }, - "type": { - "const": "documents", - "type": "string" - } - }, - "required": [ - "type", - "documents" - ], - "type": "object" - }, - { - "properties": { - "channel_id": { - "type": "string" - }, - "channel_name": { - "type": [ - "string", - "null" - ] - }, - "transcript": { - "type": "string" - }, - "type": { - "const": "channel", - "type": "string" - } - }, - "required": [ - "type", - "channel_id", - "transcript" - ], - "type": "object" - }, - { - "properties": { - "conversation": { - "items": { - "properties": { - "chat_id": { - "type": "string" - }, - "messages": { - "items": { - "properties": { - "attachment_summaries": { - "items": { - "oneOf": [ - { - "additionalProperties": false, - "properties": { - "Summary": { - "properties": { - "created_at": { - "format": "date-time", - "type": [ - "string", - "null" - ] - }, - "document_id": { - "type": "string" - }, - "id": { - "type": [ - "string", - "null" - ] - }, - "summary": { - "type": "string" - }, - "version_id": { - "type": "string" - } - }, - "required": [ - "document_id", - "summary", - "version_id" - ], - "type": "object" - } - }, - "required": [ - "Summary" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "NoSummary": { - "properties": { - "document_id": { - "type": "string" - } - }, - "required": [ - "document_id" - ], - "type": "object" - } - }, - "required": [ - "NoSummary" - ], - "type": "object" - } - ] - }, - "type": "array" - }, - "content": { - "type": "string" - }, - "date": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "content", - "date", - "attachment_summaries" - ], - "type": "object" - }, - "type": "array" - }, - "title": { - "type": "string" - } - }, - "required": [ - "chat_id", - "title", - "messages" - ], - "type": "object" - }, - "type": "array" - }, - "type": { - "const": "chat", - "type": "string" - } + "exhaustiveSearch": { + "additionalProperties": false, + "default": false, + "description": "Exhaustive search to get all results. Defaults to false. Set to true when you need all matching documents, ignoring pagination limits.", + "type": "boolean" + }, + "fileTypes": { + "additionalProperties": false, + "description": "Document file types to include. Examples: ['pdf'], ['md', 'txt']. Leave empty to include all document types.", + "items": { + "additionalProperties": false, + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "minAccessLevel": { + "additionalProperties": false, + "description": "Minimum access level required. Defaults to 'view' if not specified.", + "enum": [ + "view", + "comment", + "edit", + "owner", + null + ], + "type": [ + "string", + "null" + ] + }, + "pageOffset": { + "additionalProperties": false, + "default": 0, + "description": "Page offset for pagination. Default is 0. Use higher values to get subsequent pages of results.", + "format": "int64", + "type": "integer" + }, + "pageSize": { + "additionalProperties": false, + "default": 50, + "description": "Number of results per page. Max is 100, default is 50. Use smaller values for focused results.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "exhaustiveSearch", + "fileTypes", + "minAccessLevel", + "pageOffset", + "pageSize" + ], + "title": "ListDocuments", + "type": "object" + }, + "outputSchema": { + "properties": { + "results": { + "description": "The list results (truncated to `results_returned` limit if applicable)", + "items": { + "properties": { + "access_level": { + "description": "The user's access level to this document", + "enum": [ + "view", + "comment", + "edit", + "owner" + ], + "type": "string" }, - "required": [ - "type", - "conversation" - ], - "type": "object" - }, - { - "properties": { - "messages": { - "items": { - "properties": { - "bcc": { - "items": { - "type": "string" - }, - "type": "array" - }, - "cc": { - "items": { - "type": "string" - }, - "type": "array" - }, - "content": { - "type": "string" - }, - "messageId": { - "type": "string" - }, - "recipients": { - "items": { - "type": "string" - }, - "type": "array" - }, - "sender": { - "type": "string" - }, - "sentAt": { - "format": "date-time", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "messageId", - "sender", - "recipients", - "cc", - "bcc", - "content" - ], - "type": "object" - }, - "type": "array" - }, - "subject": { - "type": [ - "string", - "null" - ] - }, - "thread_id": { - "type": "string" - }, - "type": { - "const": "email", - "type": "string" - } + "created_at": { + "description": "When the document was created", + "format": "date-time", + "type": "string" }, - "required": [ - "type", - "thread_id", - "messages" - ], - "type": "object" - }, - { - "properties": { - "formatted_preview": { - "type": "string" - }, - "type": { - "const": "itemPreviews", - "type": "string" - } + "deleted_at": { + "description": "When the document was deleted (null if not deleted)", + "format": "date-time", + "type": [ + "string", + "null" + ] }, - "required": [ - "type", - "formatted_preview" - ], - "type": "object" - } - ] + "document_id": { + "description": "The document id", + "type": "string" + }, + "document_name": { + "description": "The document name", + "type": "string" + }, + "file_type": { + "description": "The file type of the document", + "type": [ + "string", + "null" + ] + }, + "owner": { + "description": "The owner of the document", + "type": "string" + }, + "project_id": { + "description": "The project id containing the document", + "type": [ + "string", + "null" + ] + }, + "updated_at": { + "description": "When the document was last updated", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "document_id", + "document_name", + "owner", + "created_at", + "updated_at", + "access_level" + ], + "type": "object" + }, + "type": "array" + }, + "resultsReturned": { + "description": "The number of results returned", + "format": "uint", + "minimum": 0, + "type": "integer" + }, + "totalResults": { + "description": "Total number of results found", + "format": "uint", + "minimum": 0, + "type": "integer" } }, "required": [ - "content" + "results", + "totalResults", + "resultsReturned" ], - "title": "ReadResponse", + "title": "ListDocumentsResponse", "type": "object" } }, @@ -1846,6 +1971,72 @@ "title": "AIDiffResponse", "type": "object" } + }, + { + "name": "web_search", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "This is the expected shape of the streamed json following a `server_tool_use` in content_block_start event", + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "WebSearchToolCall", + "type": "object" + }, + "outputSchema": { + "$defs": { + "SearchResult": { + "description": "A single search result from web search", + "oneOf": [ + { + "properties": { + "title": { + "type": "string" + }, + "type": { + "const": "web_search_result", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "url" + ], + "type": "object" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Web search response content returned by Claude when using the web_search tool", + "properties": { + "content": { + "description": "The search query that was executed\nArray of search results", + "items": { + "$ref": "#/$defs/SearchResult" + }, + "type": "array" + }, + "tool_use_id": { + "type": "string" + } + }, + "required": [ + "content", + "tool_use_id" + ], + "title": "WebSearchResponse", + "type": "object" + } } ] } \ No newline at end of file diff --git a/rust/cloud-storage/document_cognition_service/src/api/mod.rs b/rust/cloud-storage/document_cognition_service/src/api/mod.rs index 035d648a33..62854557a7 100644 --- a/rust/cloud-storage/document_cognition_service/src/api/mod.rs +++ b/rust/cloud-storage/document_cognition_service/src/api/mod.rs @@ -27,7 +27,6 @@ mod attachments; mod chats; mod macros; mod notification; -pub mod tools; #[tracing::instrument(err, skip(state))] pub async fn setup_and_serve(state: ApiContext) -> anyhow::Result<()> { @@ -86,7 +85,6 @@ fn api_router(api_context: ApiContext) -> Router { .nest("/citations", citations::router()) .nest("/preview", preview::router()) .with_state(api_context.clone()) - .nest("/tools", tools::router()) .nest("/completions", completions::router()) .nest("/models", models::router()) .layer( diff --git a/rust/cloud-storage/document_cognition_service/src/api/swagger.rs b/rust/cloud-storage/document_cognition_service/src/api/swagger.rs index 19da5ab8c8..d528863eab 100644 --- a/rust/cloud-storage/document_cognition_service/src/api/swagger.rs +++ b/rust/cloud-storage/document_cognition_service/src/api/swagger.rs @@ -23,7 +23,6 @@ use crate::{ }, models::get_models, preview::get_batch_preview, - tools, ws::{self}, }, model::{ @@ -109,7 +108,6 @@ use utoipa::OpenApi; revert_delete_chat::handler, chat_history::get_chat_history_handler, chat_history_batch_messages::get_chat_history_batch_messages_handler, - tools::get_tool_schemas, ), components( schemas( @@ -207,8 +205,8 @@ use utoipa::OpenApi; GetCompletionResponse, // Tools - tools::ToolSchemasResponse, - tools::ToolSchema + ai::tool::schema::ToolSchema, + ai::tool::schema::ToolSchemas, ), ), tags( diff --git a/rust/cloud-storage/document_cognition_service/src/api/tools/mod.rs b/rust/cloud-storage/document_cognition_service/src/api/tools/mod.rs deleted file mode 100644 index e7a455a676..0000000000 --- a/rust/cloud-storage/document_cognition_service/src/api/tools/mod.rs +++ /dev/null @@ -1,52 +0,0 @@ -use axum::{Router, http::StatusCode, response::Json, routing::get}; -use serde::Serialize; -use utoipa::ToSchema; - -#[derive(Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ToolSchemasResponse { - pub schemas: Vec, -} - -#[derive(Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ToolSchema { - pub name: String, - pub input_schema: serde_json::Value, - pub output_schema: serde_json::Value, -} - -pub fn tool_schemas() -> ToolSchemasResponse { - ToolSchemasResponse { - schemas: ai_tools::all_tools() - .toolset - .tools - .iter() - .map(|(name, tool_object)| ToolSchema { - name: name.clone(), - input_schema: tool_object.input_schema.clone(), - output_schema: tool_object.output_schema.clone(), - }) - .collect(), - } -} - -/// Get all available tool schemas as JSON Schema definitions -#[utoipa::path( - get, - path = "/tools/schemas", - responses( - (status = 200, description = "Tool schemas retrieved successfully", body = ToolSchemasResponse), - (status = 500, description = "Internal server error") - ), - tag = "tools" -)] - -pub async fn get_tool_schemas() -> Result, StatusCode> { - let schemas = tool_schemas(); - Ok(Json(schemas)) -} - -pub fn router() -> Router { - Router::new().route("/schemas", get(get_tool_schemas)) -} diff --git a/rust/cloud-storage/document_cognition_service/src/main.rs b/rust/cloud-storage/document_cognition_service/src/main.rs index 19c488c4f3..a5e023ead2 100644 --- a/rust/cloud-storage/document_cognition_service/src/main.rs +++ b/rust/cloud-storage/document_cognition_service/src/main.rs @@ -35,7 +35,7 @@ async fn main() -> anyhow::Result<()> { if matches!(config.environment, Environment::Local) { let tool_schemas = - serde_json::to_string_pretty(&api::tools::tool_schemas()).expect("tool schemas"); + serde_json::to_string_pretty(&ai_tools::all_tool_schemas()).expect("tool schemas"); std::fs::write("schemas/tools.json", tool_schemas).expect("write_tool_schema"); }