From 827e06b4b3dd7b2b970b3a0a37e3dfbe98d1fafd Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:59:47 +0200 Subject: [PATCH] feat(aichat): add logs api endpoint as tool (#6197) * add logs * add logs search + better load tools logic * use json * nit * only add for ee * nit * filter out search after first fail * Revert "filter out search after first fail" This reverts commit 2abf0db6e5a1be84e67d1a153281b74d448cb5cd. * call endpoint to know if it is available * cleaning * Apply suggestion from @graphite-app[bot] Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * fix * draft * call enabled endpoint * not workspaced * remove from system prompt if not enterprise * fix eeref command * update ee ref --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- .github/workflows/git-commands.yaml | 10 +- backend/ee-repo-ref.txt | 2 +- .../copilot/chat/AIChatManager.svelte.ts | 23 +-- .../copilot/chat/navigator/apiTools.ts | 142 ++++++++++++++++-- .../components/copilot/chat/navigator/core.ts | 3 +- 5 files changed, 149 insertions(+), 31 deletions(-) diff --git a/.github/workflows/git-commands.yaml b/.github/workflows/git-commands.yaml index 4dbdf36ed1..9dcae33bb1 100644 --- a/.github/workflows/git-commands.yaml +++ b/.github/workflows/git-commands.yaml @@ -151,17 +151,17 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt - echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}" - # commit and push the changes - PR_NUMBER=${{ github.event.issue.number }} - BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName) echo "Checking out PR branch: $BRANCH_NAME" git checkout $BRANCH_NAME git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git config pull.rebase true git pull origin $BRANCH_NAME + echo "${{ steps.get-commit-hash.outputs.commit_hash }}" > backend/ee-repo-ref.txt + echo "Updated backend/ee-repo-ref.txt with commit hash: ${{ steps.get-commit-hash.outputs.commit_hash }}" + # commit and push the changes + PR_NUMBER=${{ github.event.issue.number }} + BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName) git add backend/ee-repo-ref.txt git commit -m "Update ee-repo-ref.txt" || echo "No changes to commit" git push origin $BRANCH_NAME diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 653071700b..3c15148a60 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -3cccbe81494f7edd113dce2812278b00c91160c0 \ No newline at end of file +64f3cdcd831b81ca42991ddc201533753d590a6b diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 2983d4b35b..78282eca09 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -89,15 +89,16 @@ class AIChatManager { open = $derived(chatState.size > 0) - constructor() { - loadApiTools() - .then((tools) => { - this.apiTools = tools - }) - .catch((err) => { - console.error('Error loading api tools', err) - this.apiTools = [] - }) + loadApiTools = async () => { + try { + this.apiTools = await loadApiTools() + if (this.mode === AIMode.NAVIGATOR) { + this.tools = [this.changeModeTool, ...navigatorTools, ...this.apiTools] + } + } catch (err) { + console.error('Error loading api tools', err) + this.apiTools = [] + } } setAiChatInput(aiChatInput: AIChatInput | null) { @@ -604,6 +605,10 @@ class AIChatManager { } } + if (this.mode === AIMode.NAVIGATOR && this.apiTools.length === 0) { + await this.loadApiTools() + } + await this.chatRequest({ ...params }) diff --git a/frontend/src/lib/components/copilot/chat/navigator/apiTools.ts b/frontend/src/lib/components/copilot/chat/navigator/apiTools.ts index f03920415f..1aea113ba6 100644 --- a/frontend/src/lib/components/copilot/chat/navigator/apiTools.ts +++ b/frontend/src/lib/components/copilot/chat/navigator/apiTools.ts @@ -1,7 +1,7 @@ import type { ChatCompletionTool } from 'openai/resources/index.mjs' import type { Tool } from '../shared' import { get } from 'svelte/store' -import { workspaceStore } from '$lib/stores' +import { workspaceStore, enterpriseLicense } from '$lib/stores' // OpenAPI type definitions interface OpenAPIParameter { @@ -54,6 +54,88 @@ interface OpenAPISpec { paths: { [path: string]: OpenAPIPathItem } + components?: { + parameters?: { + [name: string]: OpenAPIParameter + } + schemas?: { + [name: string]: any + } + } +} + +interface OpenAPIParameterWithRef { + $ref?: string + name?: string + in?: string + description?: string + required?: boolean + schema?: { + type?: string + format?: string + } +} + +/** + * Dereferences parameter $ref references in an OpenAPI spec + * Only resolves parameter references, not schemas or other components + */ +function dereferenceParameters(spec: OpenAPISpec): OpenAPISpec { + if (!spec.components?.parameters) { + return spec + } + + const resolveParameterRef = (paramRef: OpenAPIParameterWithRef): OpenAPIParameter => { + if (paramRef.$ref) { + // Extract parameter name from $ref (e.g., "#/components/parameters/WorkspaceId" -> "WorkspaceId") + const refPath = paramRef.$ref.split('/') + if (refPath.length >= 4 && refPath[1] === 'components' && refPath[2] === 'parameters') { + const paramName = refPath[3] + const resolvedParam = spec.components?.parameters?.[paramName] + if (resolvedParam) { + return resolvedParam + } + } + console.warn(`Could not resolve parameter reference: ${paramRef.$ref}`) + return paramRef as OpenAPIParameter + } + return paramRef as OpenAPIParameter + } + + const processParameters = (parameters: OpenAPIParameterWithRef[]): OpenAPIParameter[] => { + return parameters.map(resolveParameterRef) + } + + const dereferencedSpec: OpenAPISpec = { + ...spec, + paths: {} + } + + // Process each path + for (const [pathKey, pathItem] of Object.entries(spec.paths)) { + const newPathItem: OpenAPIPathItem = { ...pathItem } + + // Dereference path-level parameters + if (pathItem.parameters) { + newPathItem.parameters = processParameters(pathItem.parameters as OpenAPIParameterWithRef[]) + } + + // Dereference operation-level parameters + const methods = ['get', 'post', 'put', 'delete', 'patch', 'options'] as const + for (const method of methods) { + const operation = pathItem[method] + if (operation?.parameters) { + newPathItem[method] = { + ...operation, + parameters: processParameters(operation.parameters as OpenAPIParameterWithRef[]) + } + } + } + + dereferencedSpec.paths[pathKey] = newPathItem + } + + return dereferencedSpec } const buildApiCallTools = ( @@ -124,6 +206,10 @@ export function buildToolsFromOpenApi( // Add path parameters for (const param of pathParams) { + if (param.name === 'workspace') { + continue + } + parameters.properties[param.name] = { type: param.schema?.type || 'string', description: param.description || `Path parameter: ${param.name}` @@ -167,13 +253,13 @@ export function buildToolsFromOpenApi( } const tool = buildApiCallTools( - 'api_' + op.operationId, + 'api_' + op.operationId.replace(/\s+/g, ''), op.summary || op.description || `${method.toUpperCase()} ${path}`, parameters ) // Store the endpoint path in the map - endpointMap['api_' + op.operationId] = `${method.toUpperCase()} ${path}` + endpointMap['api_' + op.operationId.replace(/\s+/g, '')] = `${method.toUpperCase()} ${path}` tools.push(tool) } @@ -193,7 +279,6 @@ export function createApiTools( const toolName = chatTool.function.name let endpoint = endpointMap[toolName] || '' endpoint = endpoint.replace('{workspace}', get(workspaceStore) as string) - toolCallbacks.setToolStatus(toolId, `Calling API endpoint (${endpoint})...`) try { // Extract method and path from endpoint @@ -212,8 +297,8 @@ export function createApiTools( if (key === 'body') continue // Body is handled separately // Check if this is a path parameter - if (url.includes(`:${key}`)) { - url = url.replace(`:${key}`, encodeURIComponent(String(value))) + if (url.includes(`{${key}}`)) { + url = url.replace(`{${key}}`, encodeURIComponent(String(value))) } else { // Assume it's a query parameter queryParams[key] = String(value) @@ -231,18 +316,32 @@ export function createApiTools( // Log the constructed URL console.log(`Calling API: ${method} ${url} with args: ${JSON.stringify(args)}`) + toolCallbacks.setToolStatus(toolId, `Calling API endpoint (${url})...`) + const response = await fetch(url, { method: method }) - const data = await response.json() - // For now, return a placeholder response - // In a real implementation, we would make the actual API call here - toolCallbacks.setToolStatus(toolId, `API call to ${endpoint} completed`) - return JSON.stringify({ - success: true, - data: data - }) + if (response.ok) { + let result = '' + if (response.headers.get('content-type')?.includes('application/json')) { + result = await response.json() + } else { + result = await response.text() + } + toolCallbacks.setToolStatus(toolId, `API call to ${url} completed`) + return JSON.stringify({ + success: true, + data: result + }) + } else { + const text = await response.text() + toolCallbacks.setToolStatus(toolId, `API call to ${url} failed`) + return JSON.stringify({ + success: false, + data: text + }) + } } catch (error) { toolCallbacks.setToolStatus(toolId, `API call to ${endpoint} failed`) console.error(`Error calling API to ${endpoint}:`, error) @@ -256,9 +355,14 @@ export function createApiTools( export async function loadApiTools(): Promise[]> { try { const response = await fetch('/api/openapi.json') - const openApiSpec = (await response.json()) as OpenAPISpec + const rawOpenApiSpec = (await response.json()) as OpenAPISpec + + // Dereference parameter references + const openApiSpec = dereferenceParameters(rawOpenApiSpec) + const pathsToInclude = [ 'jobs', + 'jobs_u', 'scripts', 'flows', 'resources', @@ -267,6 +371,14 @@ export async function loadApiTools(): Promise[]> { 'workers' ] + // call srch endpoint to check if it's available + if (get(enterpriseLicense)) { + const srchResponse = await fetch(`/api/srch/index/search/enabled`) + if (srchResponse.ok) { + pathsToInclude.push('srch/w') // job search + } + } + const { tools: apiTools, endpointMap } = buildToolsFromOpenApi(openApiSpec, { pathFilter: (path) => pathsToInclude.some((p) => path.includes(`/${p}/`)), methodFilter: ['get'] diff --git a/frontend/src/lib/components/copilot/chat/navigator/core.ts b/frontend/src/lib/components/copilot/chat/navigator/core.ts index aecf60bc30..9194ec290d 100644 --- a/frontend/src/lib/components/copilot/chat/navigator/core.ts +++ b/frontend/src/lib/components/copilot/chat/navigator/core.ts @@ -5,7 +5,7 @@ import type { } from 'openai/resources/index.mjs' import type { Tool } from '../shared' import { ResourceService } from '$lib/gen' -import { workspaceStore } from '$lib/stores' +import { enterpriseLicense, workspaceStore } from '$lib/stores' import { get } from 'svelte/store' import { triggerablesByAi } from '../sharedChatState.svelte' @@ -29,6 +29,7 @@ INSTRUCTIONS: - If you are asked to fill a form or act on an input, input the existing json object and change the fields the user asked you to change. Take into account the prompt_for_ai field of the schema to know what and how to do changes. Then tell the user that you have updated the form, and ask him to review the changes before running the script or flow. - For form inputs where format starts with "resource-" and is not "resource-obj", fetch the available resources using get_available_resources, and then use the resource_path prefixed with "$res:" to fill the input. - If you are not sure about an input, set the ones you are sure about, and then ask the user for the value of the input you are not sure about. +${get(enterpriseLicense) ? `- If asked to look through the jobs logs, use the /srch/w/{workspace}/index/search/job endpoint to search for the relevant jobs runs. Then use /w/{workspace}/jobs_u/get to get the logs of each job.` : ''} GENERAL PRINCIPLES: - Be concise but thorough