mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
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:
co-authored by
graphite-app[bot]
parent
c076d1332e
commit
827e06b4b3
@@ -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 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user