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>
This commit is contained in:
centdix
2025-07-16 17:59:47 +00:00
committed by GitHub
co-authored by graphite-app[bot]
parent c076d1332e
commit 827e06b4b3
5 changed files with 149 additions and 31 deletions
+5 -5
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
3cccbe81494f7edd113dce2812278b00c91160c0
64f3cdcd831b81ca42991ddc201533753d590a6b
@@ -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
})
@@ -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<Tool<{}>[]> {
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<Tool<{}>[]> {
'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']
@@ -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