Skip to content

Commit 43e2aa2

Browse files
committed
Multiple minor changes to ai agent utilisation
1 parent 439b0c1 commit 43e2aa2

File tree

4 files changed

+172
-35
lines changed

4 files changed

+172
-35
lines changed

ai.go

Lines changed: 156 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
//var model = "gpt-4o-mini"
3737
//var model = "o4-mini"
3838
var standalone bool
39+
//var model = "gpt-5-mini"
3940
var model = "gpt-5-mini"
4041
var fallbackModel = ""
4142
var assistantId = os.Getenv("OPENAI_ASSISTANT_ID")
@@ -1268,6 +1269,11 @@ func AutofixAppLabels(app WorkflowApp, label string, keys []string) (WorkflowApp
12681269
return app, WorkflowAppAction{}
12691270
}
12701271

1272+
if strings.TrimSpace(strings.ToLower(label)) == "api" || label == "custom_action" {
1273+
log.Printf("[INFO] Skipping label '%s' in AutofixAppLabels for app %s (%s) as it's too generic", label, app.Name, app.ID)
1274+
return app, WorkflowAppAction{}
1275+
}
1276+
12711277
// // Double check if it has it or not
12721278
parsedLabel := strings.ToLower(strings.ReplaceAll(label, " ", "_"))
12731279
for _, action := range app.Actions {
@@ -1281,7 +1287,6 @@ func AutofixAppLabels(app WorkflowApp, label string, keys []string) (WorkflowApp
12811287

12821288
// Fix the label to be as it is in category (uppercase + spaces)
12831289
// fml, there is no consistency to casing + underscores, so we keep the new
1284-
12851290
log.Printf("[INFO][AI] Running app fix for label '%s' for app %s (%s) with %d actions", label, app.Name, app.ID, len(app.Actions))
12861291

12871292
// Just a reset, as Other doesn't really achieve anything directly
@@ -6313,7 +6318,6 @@ func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action)
63136318
// Metadata = org-specific context
63146319
// This e.g. makes "me" mean "users in my org" and such
63156320
metadata := ""
6316-
63176321
if len(execution.Workflow.UpdatedBy) > 0 {
63186322
metadata += fmt.Sprintf("Current person: %s\n", execution.Workflow.UpdatedBy)
63196323
}
@@ -6344,17 +6348,141 @@ func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action)
63446348
if len(users) > 0 {
63456349
metadata += fmt.Sprintf("users: %s\n", strings.Join(users, ", "))
63466350
}
6351+
6352+
decidedApps := ""
6353+
appauth, autherr := GetAllWorkflowAppAuth(ctx, org.Id)
6354+
if autherr == nil && len(appauth) > 0 {
6355+
preferredApps := []WorkflowApp{}
6356+
if len(org.SecurityFramework.SIEM.Name) > 0 {
6357+
preferredApps = append(preferredApps, WorkflowApp{
6358+
Categories: []string{"siem"},
6359+
Name: org.SecurityFramework.SIEM.Name,
6360+
})
6361+
}
6362+
6363+
if len(org.SecurityFramework.EDR.Name) > 0 {
6364+
//preferredApps += strings.ToLower(org.SecurityFramework.EDR.Name) + ", "
6365+
preferredApps = append(preferredApps, WorkflowApp{
6366+
Categories: []string{"eradication"},
6367+
Name: org.SecurityFramework.EDR.Name,
6368+
})
6369+
}
6370+
6371+
if len(org.SecurityFramework.Communication.Name) > 0 {
6372+
//preferredApps += strings.ToLower(org.SecurityFramework.Cases.Name) + ", "
6373+
6374+
preferredApps = append(preferredApps, WorkflowApp{
6375+
Categories: []string{"cases"},
6376+
Name: org.SecurityFramework.Communication.Name,
6377+
})
6378+
}
6379+
6380+
if len(org.SecurityFramework.Cases.Name) > 0 {
6381+
//preferredApps += strings.ToLower(org.SecurityFramework.Cases.Name) + ", "
6382+
6383+
preferredApps = append(preferredApps, WorkflowApp{
6384+
Categories: []string{"cases"},
6385+
Name: org.SecurityFramework.Cases.Name,
6386+
})
6387+
}
6388+
6389+
if len(org.SecurityFramework.Assets.Name) > 0 {
6390+
//preferredApps += strings.ToLower(org.SecurityFramework.Assets.Name) + ", "
6391+
6392+
preferredApps = append(preferredApps, WorkflowApp{
6393+
Categories: []string{"assets"},
6394+
Name: org.SecurityFramework.Assets.Name,
6395+
})
6396+
}
6397+
6398+
if len(org.SecurityFramework.Network.Name) > 0 {
6399+
//preferredApps += strings.ToLower(org.SecurityFramework.Network.Name) + ", "
6400+
6401+
preferredApps = append(preferredApps, WorkflowApp{
6402+
Categories: []string{"network"},
6403+
Name: org.SecurityFramework.Network.Name,
6404+
})
6405+
}
6406+
6407+
if len(org.SecurityFramework.Intel.Name) > 0 {
6408+
//preferredApps += strings.ToLower(org.SecurityFramework.Intel.Name) + ", "
6409+
6410+
preferredApps = append(preferredApps, WorkflowApp{
6411+
Categories: []string{"intel"},
6412+
Name: org.SecurityFramework.Intel.Name,
6413+
})
6414+
}
6415+
6416+
if len(org.SecurityFramework.IAM.Name) > 0 {
6417+
//preferredApps += strings.ToLower(org.SecurityFramework.IAM.Name) + ", "
6418+
preferredApps = append(preferredApps, WorkflowApp{
6419+
Categories: []string{"iam"},
6420+
Name: org.SecurityFramework.IAM.Name,
6421+
})
6422+
}
6423+
6424+
for _, auth := range appauth {
6425+
// ALWAYS append valid auth
6426+
if !auth.Validation.Valid {
6427+
continue
6428+
}
6429+
6430+
//lowerName := strings.ToLower(auth.App.Name)
6431+
log.Printf("APP CATEGORIES: %s\n", auth.App.Categories)
6432+
if len(auth.App.Categories) > 0 {
6433+
found := false
6434+
for _, preApp := range preferredApps {
6435+
if len(preApp.Categories) == 0 {
6436+
continue
6437+
}
6438+
6439+
if ArrayContains(preApp.Categories, strings.ToLower(auth.App.Categories[0]) ) {
6440+
found = true
6441+
break
6442+
}
6443+
}
6444+
6445+
if found {
6446+
continue
6447+
}
6448+
}
6449+
6450+
preferredApps = append(preferredApps, auth.App)
6451+
}
6452+
6453+
// FIXME: Pre-filter before this to ensure we have good
6454+
// apps ONLY.
6455+
for _, preferredApp := range preferredApps {
6456+
if len(preferredApp.Name) == 0 {
6457+
continue
6458+
}
6459+
6460+
lowername := strings.ToLower(preferredApp.Name)
6461+
if strings.Contains(decidedApps, lowername) {
6462+
continue
6463+
}
6464+
6465+
decidedApps += lowername + ", "
6466+
}
6467+
}
6468+
6469+
if len(decidedApps) > 0 {
6470+
metadata += fmt.Sprintf("preferred tools: %s\n", decidedApps)
6471+
}
6472+
6473+
63476474
}
63486475

63496476
}
63506477

63516478
// Create the OpenAI body struct
6352-
systemMessage := `You are a general AI agent. You can make decisions based on the user input. You should output a list of decisions based on the input. Available actions within categories you can choose from are below. Only use built-in actions such as analyze (ai analysis) or ask (human analysis) if it makes sense. If you need to ask for input multiple times in a row, ask both questions at the same time. Only ask if the User context doesn't contain the details you need. Assume authentication already exists for all your tools.
6479+
systemMessage := `You are a general AI agent which makes decisions based on user input. You should output a list of decisions based on the same input. Available actions within categories you can choose from are below. Only use built-in actions such as analyze (ai analysis) or ask (human analysis) if it is absolutely necessary. If you need to ask for input multiple times in a row, ask both questions at the same time. Only ask if the User context doesn't contain the details you need, AND the question isn't about networking or authentication.
63536480
6354-
standalone actions:
6355-
ask
6481+
# agent actions:
6482+
- ask
63566483
6357-
allowed actions: `
6484+
# integration actions:
6485+
`
63586486
userMessage := ""
63596487

63606488
// Don't think this matters much
@@ -6383,7 +6511,7 @@ allowed actions: `
63836511
continue
63846512
}
63856513

6386-
systemMessage += fmt.Sprintf("%s\n", strings.ReplaceAll(actionStr, " " , "_"))
6514+
systemMessage += fmt.Sprintf("- %s\n", strings.ReplaceAll(actionStr, " " , "_"))
63876515

63886516
}
63896517

@@ -6414,28 +6542,30 @@ allowed actions: `
64146542
// Will just have to make a translation system.
64156543
//typeOptions := []string{"ask", "singul", "workflow", "agent"}
64166544
typeOptions := []string{"ask", "singul"}
6417-
extraString := "Have a MINIMUM of two decisions. "
6545+
extraString := "Return a MINIMUM of two decisions in a JSON array. "
64186546
if len(typeOptions) == 0 {
64196547
extraString = ""
64206548
}
64216549

6422-
systemMessage += fmt.Sprintf(`. Available categories (default: singul): %s. If you are unsure about a decision, always ask for user input. The output should be an ordered JSON list in the format [{"i": 0, "category": "singul", "action": "action_name", "tool": "<tool name>", "confidence": 0.95, "runs": "1", "reason": "Short reason why", "fields": [{"key": "max_results", "value": "5"}, {"key": "body", "value": "$action_name"}] WITHOUT newlines. The reason should be concise and understandable to a user, and should not include unnecessary details.
6550+
systemMessage += fmt.Sprintf(`Available categories (default: singul): %s. If you are unsure about a decision, always ask for user input. The output should be an ordered JSON list in the format [{"i": 0, "category": "singul", "action": "action_name", "tool": "<tool name>", "confidence": 0.95, "runs": "1", "reason": "Short reason why", "fields": [{"key": "max_results", "value": "5"}, {"key": "body", "value": "$action_name"}] WITHOUT newlines. The reason should be concise and understandable to a user, and should not include unnecessary details.
64236551
6424-
User Context:
6552+
# User Context:
64256553
%s
64266554
6427-
Formatting Rules:
6555+
# Formatting Rules:
64286556
- Do NOT ask to narrow down the scope unless ABSOLUTELY necessary. Assume all the information is already in place.
64296557
- If a tool or app is mentioned, add it to the tool field. Otherwise make the field empty.
64306558
- Indexes should be the same if they should run in parallell.
64316559
- The confidence is between 0 and 1.
64326560
- Runs are how many times it should run requests (default: 1, * for all looped items).
64336561
- The {{action_name}} has to match EXACTLY the action name of a previous decision.
64346562
- NEVER add unnecessary fields to the fields array, only add the ones that are absolutely needed for the action to run!
6435-
- If you ask for user input, use the "ask" action and add a "question" field.
6436-
- Do NOT add empty decisions for now reason.
6563+
- If you ask for user input, use the "ask" action and add a "question" field. Do NOT use this for authentication.
6564+
- If you use the "API" action, make sure to fill the "tool". Additionally fill in "url", "method" and "body" fields.
6565+
- Do NOT add empty decisions for no reason.
64376566
6438-
Decision Rules:
6567+
# Decision Rules:
6568+
- NEVER ask for usernames, apikeys or other authentication information from the user.
64396569
- Do NOT add random fields and do NOT guess formatting e.g. body formatting
64406570
- Fields can be set manually, or use previous action output by adding them in the format {{action_name}}, such as {"key": "body": "value": "{{tickets[0].fieldname}}} to get the first 'ticket' fieldname from a previous decision.
64416571
@@ -6447,7 +6577,7 @@ Decision Rules:
64476577
//Model: "gpt-4o-mini",
64486578
//Model: "gpt-4.1-mini",
64496579
//Model: "o4-mini", // "gpt-4o-mini" is the same as "4o-mini" in OpenAI API
6450-
Model: "gpt-5-mini", // "gpt-4o-mini" is the same as "4o-mini" in OpenAI API
6580+
Model: "gpt-5-nano", // "gpt-4o-mini" is the same as "4o-mini" in OpenAI API
64516581
Messages: []openai.ChatCompletionMessage{
64526582
{
64536583
Role: openai.ChatMessageRoleSystem,
@@ -6461,11 +6591,13 @@ Decision Rules:
64616591
//Temperature: 0.95, // Adds a tiny bit of randomness
64626592
Temperature: 1,
64636593

6594+
// json_object -> tends to want a single item and not an array
6595+
//ResponseFormat: &openai.ChatCompletionResponseFormat{
6596+
// Type: "json_object",
6597+
//},
6598+
64646599
// Reasoning control
64656600
MaxCompletionTokens: 5000,
6466-
ResponseFormat: &openai.ChatCompletionResponseFormat{
6467-
Type: "json_object",
6468-
},
64696601
ReasoningEffort: "medium",
64706602
Store: true,
64716603
}
@@ -6741,7 +6873,7 @@ Decision Rules:
67416873

67426874
// Edgecase handling for LLM not being available etc
67436875
if len(choicesString) > 0 {
6744-
log.Printf("\n\n[ERROR][%s] Found choicesString in AI Agent response error handling: %s\n\n", execution.ExecutionId, choicesString)
6876+
log.Printf("\n\n[ERROR][%s] Found choicesString (1) in AI Agent response error handling: %s\n\n", execution.ExecutionId, choicesString)
67456877

67466878
} else if len(openaiOutput.Choices) == 0 {
67476879
log.Printf("[ERROR][%s] No choices found in AI agent response. Status: %d. Raw: %s", execution.ExecutionId, outputMap.Status, bodyString)
@@ -6760,7 +6892,7 @@ Decision Rules:
67606892
} else {
67616893
choicesString = openaiOutput.Choices[0].Message.Content
67626894
if debug {
6763-
log.Printf("[DEBUG] Found choices string: %s", choicesString)
6895+
log.Printf("[DEBUG] Found choices string (2) - len: %d: %s", len(choicesString), choicesString)
67646896
}
67656897

67666898
// Handles reasoning models for Refusal control edgecases
@@ -6886,7 +7018,7 @@ Decision Rules:
68867018
// Which do we use:
68877019
// 1. Local Singul
68887020
if decision.Action == "" {
6889-
log.Printf("[ERROR] No action found in AI agent decision")
7021+
log.Printf("[ERROR] No action found in AI agent decision: %#v", decision)
68907022
continue
68917023
}
68927024

@@ -7216,10 +7348,10 @@ func RunAiQuery(systemMessage, userMessage string, incomingRequest ...openai.Cha
72167348
MaxTokens: maxTokens,
72177349
}
72187350

7219-
// Too specific, but.. :)
7220-
if model == "o4-mini" || model == "gpt-5-mini" {
7351+
// FIXME: Too specific. Should be self-corrective.. :)
7352+
if chatCompletion.MaxTokens > 0 && (model == "o4-mini" || model == "gpt-5-mini" || model == "gpt-5-nano") {
7353+
chatCompletion.MaxCompletionTokens = chatCompletion.MaxTokens
72217354
chatCompletion.MaxTokens = 0
7222-
chatCompletion.MaxCompletionTokens = maxTokens
72237355
}
72247356

72257357
// Rerun with the same chat IF POSSIBLE

cloudSync.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,10 +2056,10 @@ func RunAgentDecisionSingulActionHandler(execution WorkflowExecution, decision A
20562056

20572057
url := fmt.Sprintf("%s/api/v1/apps/categories/run?authorization=%s&execution_id=%s", baseUrl, execution.Authorization, execution.ExecutionId)
20582058

2059-
// Change timeout to be 30 seconds (just in case)
2059+
// Change timeout to be 300 seconds (just in case)
2060+
// Allows for reruns and self-correcting
20602061
client := GetExternalClient(url)
2061-
client.Timeout = 60 * time.Second
2062-
2062+
client.Timeout = 300 * time.Second
20632063
parsedFields := TranslateBadFieldFormats(decision.Fields)
20642064
parsedAction := CategoryAction{
20652065
AppName: decision.Tool,
@@ -2090,7 +2090,7 @@ func RunAgentDecisionSingulActionHandler(execution WorkflowExecution, decision A
20902090
req.Header.Set("Content-Type", "application/json")
20912091
resp, err := client.Do(req)
20922092
if err != nil {
2093-
log.Printf("[ERROR][%s] Failed running agent decision: %s", execution.ExecutionId, err)
2093+
log.Printf("[ERROR][%s] Failed running agent decision (1). Timeout: %d: %s", execution.ExecutionId, client.Timeout, err)
20942094
return []byte{}, debugUrl, err
20952095
}
20962096

@@ -2164,11 +2164,11 @@ func RunAgentDecisionSingulActionHandler(execution WorkflowExecution, decision A
21642164

21652165
if resp.StatusCode != 200 {
21662166
log.Printf("[ERROR][%s] Failed running agent decision with status %d: %s", execution.ExecutionId, resp.StatusCode, string(body))
2167-
return body, debugUrl, errors.New(fmt.Sprintf("Failed running agent decision. Status code %d", resp.StatusCode))
2167+
return body, debugUrl, errors.New(fmt.Sprintf("Failed running agent decision (2). Status code %d", resp.StatusCode))
21682168
}
21692169

21702170
if outputMapped.Success == false {
2171-
return originalBody, debugUrl, errors.New("Failed running agent decision. Success false for Singul action")
2171+
return originalBody, debugUrl, errors.New("Failed running agent decision (3). Success false for Singul action")
21722172
}
21732173

21742174
/*
@@ -2236,7 +2236,7 @@ func RunAgentDecisionAction(execution WorkflowExecution, agentOutput AgentOutput
22362236
decision.RunDetails.Status = "FAILURE"
22372237

22382238
if len(decision.RunDetails.RawResponse) == 0 {
2239-
decision.RunDetails.RawResponse = fmt.Sprintf("Failed to start action. Raw Error: %s", err)
2239+
decision.RunDetails.RawResponse = fmt.Sprintf("Failed to start decision action. Raw Error: %s", err)
22402240
}
22412241
} else {
22422242
decision.RunDetails.Status = "FINISHED"

codegen.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ func MakePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
716716
headerParserCode = "if isinstance(headers, dict):\n request_headers = headers\n elif len(headers) > 0:\n for header in str(headers).split(\"\\n\"):\n if ':' in header:\n headersplit=header.split(':')\n request_headers[headersplit[0].strip()] = ':'.join(headersplit[1:]).strip()\n elif '=' in header:\n headersplit=header.split('=')\n request_headers[headersplit[0].strip()] = '='.join(headersplit[1:]).strip()"
717717

718718
} else if strings.Contains(param, "queries=") {
719-
queryParserCode = "\n if len(queries) > 0:\n if queries[0] == \"?\" or queries[0] == \"&\":\n queries = queries[1:len(queries)]\n if queries[len(queries)-1] == \"?\" or queries[len(queries)-1] == \"&\":\n queries = queries[0:-1]\n for query in queries.split(\"&\"):\n if isinstance(query, list) or isinstance(query, dict):\n try:\n query = json.dumps(query)\n except:\n pass\n if '=' in query:\n headersplit=query.split('=')\n params[requests.utils.quote(headersplit[0].strip())] = requests.utils.quote(headersplit[1].strip())\n else:\n params[requests.utils.quote(query.strip())] = None\n params = '&'.join([k if v is None else f\"{k}={v}\" for k, v in params.items()])"
719+
queryParserCode = "\n if len(queries) > 0:\n if isinstance(queries, dict):\n params=queries\n else:\n if queries[0] == \"?\" or queries[0] == \"&\":\n queries = queries[1:len(queries)]\n if queries[len(queries)-1] == \"?\" or queries[len(queries)-1] == \"&\":\n queries = queries[0:-1]\n for query in queries.split(\"&\"):\n if isinstance(query, list) or isinstance(query, dict):\n try:\n query = json.dumps(query)\n except:\n pass\n if '=' in query:\n headersplit=query.split('=')\n params[requests.utils.quote(headersplit[0].strip())] = requests.utils.quote(headersplit[1].strip())\n else:\n params[requests.utils.quote(query.strip())] = None\n params = '&'.join([k if v is None else f\"{k}={v}\" for k, v in params.items()])"
720720

721721
} else {
722722
if !strings.Contains(url, fmt.Sprintf("{%s}", param)) {
@@ -892,6 +892,11 @@ func MakePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
892892
parsedDataCurlParser = `parsed_curl_command += f""" -d '{body}'""" if isinstance(body, str) else f""" -d '{body.decode("utf-8")}'"""`
893893
}
894894

895+
// Makes sure to reformat references
896+
if !strings.HasPrefix(parsedParameters, ",") {
897+
parsedParameters = fmt.Sprintf(", %s", parsedParameters)
898+
}
899+
895900
data := fmt.Sprintf(` def %s(self%s):
896901
print(f"Started function %s")
897902
params={}
@@ -1039,7 +1044,7 @@ func MakePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet
10391044
)
10401045

10411046
// Use lowercase when checking
1042-
if strings.Contains(strings.ToLower(functionname), "delete_revoke_user_role") {
1047+
if strings.Contains(strings.ToLower(functionname), "get_list_all_issues") {
10431048
log.Printf("\n%s", data)
10441049
}
10451050

shared.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4045,7 +4045,7 @@ func GetWorkflowExecutionsV2(resp http.ResponseWriter, request *http.Request) {
40454045

40464046
if user.Id != workflow.Owner || len(user.Id) == 0 {
40474047
if workflow.OrgId == user.ActiveOrg.Id {
4048-
log.Printf("[AUDIT] User %s (%s) is accessing workflow '%s' (%s) executions as %s (get executions)", user.Username, user.Id, workflow.Name, workflow.ID, user.Role)
4048+
log.Printf("[AUDIT] User %s (%s) is accessing workflow '%s' (%s) executions V2 as %s (get executions)", user.Username, user.Id, workflow.Name, workflow.ID, user.Role)
40494049
} else if project.Environment == "cloud" && user.Verified == true && user.Active == true && user.SupportAccess == true && strings.HasSuffix(user.Username, "@shuffler.io") {
40504050
log.Printf("[AUDIT] Letting verified support admin %s access workflow execs (V2) for %s", user.Username, fileId)
40514051
checkExecOrg = false
@@ -16929,7 +16929,7 @@ func ParsedExecutionResult(ctx context.Context, workflowExecution WorkflowExecut
1692916929
}
1693016930
}
1693116931

16932-
log.Printf("[INFO][%s] Updating %s (%s) in workflow from %s to %s", workflowExecution.ExecutionId, actionResult.Action.Name, actionResult.Action.ID, workflowExecution.Results[outerindex].Status, actionResult.Status)
16932+
log.Printf("[INFO][%s] Updating '%s' (%s) in workflow from %s to %s", workflowExecution.ExecutionId, actionResult.Action.Name, actionResult.Action.ID, workflowExecution.Results[outerindex].Status, actionResult.Status)
1693316933

1693416934
if workflowExecution.Results[outerindex].Status != actionResult.Status {
1693516935
dbSave = true

0 commit comments

Comments
 (0)