Skip to content

Commit 25e7d93

Browse files
committed
feat: add fluent workflow
This is equivalent to #228 but with the data extraction parts removed. We will revisit that in a separate PR.
1 parent adb91b9 commit 25e7d93

File tree

8 files changed

+303
-23
lines changed

8 files changed

+303
-23
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v1.1.0
1+
v1.2.2

main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package main
2424

2525
import (
2626
_ "embed"
27+
"strings"
2728

2829
"github.com/ansys/aali-sharedtypes/pkg/config"
2930
"github.com/ansys/aali-sharedtypes/pkg/logging"
@@ -33,6 +34,9 @@ import (
3334
"github.com/ansys/aali-flowkit/pkg/internalstates"
3435
)
3536

37+
//go:embed VERSION
38+
var version string
39+
3640
//go:embed pkg/externalfunctions/dataextraction.go
3741
var dataExtractionFile string
3842

@@ -69,6 +73,9 @@ var mcpFile string
6973
//go:embed pkg/externalfunctions/rhsc.go
7074
var rhscFile string
7175

76+
//go:embed pkg/externalfunctions/fluent.go
77+
var fluentFile string
78+
7279
func init() {
7380
// initialize config
7481
config.InitConfig([]string{}, map[string]interface{}{
@@ -83,6 +90,9 @@ func init() {
8390

8491
// initialize logging
8592
logging.InitLogger(config.GlobalConfig)
93+
94+
// assign the version from the embedded file
95+
internalstates.FlowkitVersion = strings.TrimSpace(version)
8696
}
8797

8898
func main() {
@@ -103,6 +113,7 @@ func main() {
103113
"auth": authFile,
104114
"mcp": mcpFile,
105115
"rhsc": rhscFile,
116+
"fluent": fluentFile,
106117
}
107118

108119
// Load function definitions

pkg/externalfunctions/ansysgpt.go

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ package externalfunctions
2424

2525
import (
2626
"bytes"
27+
"encoding/base64"
2728
"encoding/json"
2829
"fmt"
30+
"io"
2931
"net/http"
32+
"net/url"
3033
"regexp"
3134
"sort"
3235
"strconv"
@@ -897,6 +900,99 @@ func AecGetContextFromRetrieverModule(
897900
return context
898901
}
899902

903+
// GetContextFromDataPlugin retrieves context from a data plugin
904+
//
905+
// Tags:
906+
// - @displayName: Get Context from Data Plugin
907+
//
908+
// Parameters:
909+
// - userQuery: the user query
910+
// - apiUrl: the API URL of the data plugin
911+
// - username: the username for authentication at the data plugin
912+
// - password: the password for authentication at the data plugin
913+
// - topK: the number of results to be returned
914+
//
915+
// Returns:
916+
// - context: the context retrieved from the data plugin
917+
func GetContextFromDataPlugin(userQuery string, apiUrl string, username string, password string, topK int) (context []sharedtypes.AnsysGPTRetrieverModuleChunk) {
918+
// Encode the query and max_doc in base64
919+
encodedQuery := base64.StdEncoding.EncodeToString([]byte(userQuery))
920+
encodedMaxDoc := base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(topK)))
921+
922+
// Prepare form data
923+
formData := url.Values{}
924+
formData.Set("q", encodedQuery)
925+
formData.Set("max_doc", encodedMaxDoc)
926+
927+
// Create the request
928+
req, err := http.NewRequest("POST", apiUrl, bytes.NewBufferString(formData.Encode()))
929+
if err != nil {
930+
panic(fmt.Errorf("error creating request: %v", err))
931+
}
932+
933+
// Set content type for form data
934+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
935+
936+
// Set basic authentication header
937+
auth := username + ":" + password
938+
encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth))
939+
req.Header.Set("Authorization", "Basic "+encodedAuth)
940+
941+
// Make the request
942+
client := &http.Client{}
943+
resp, err := client.Do(req)
944+
if err != nil {
945+
panic(fmt.Errorf("error making request: %v", err))
946+
}
947+
defer resp.Body.Close()
948+
949+
// Read the response body
950+
body, err := io.ReadAll(resp.Body)
951+
if err != nil {
952+
panic(fmt.Errorf("error reading response body: %v", err))
953+
}
954+
955+
// Check status code
956+
if resp.StatusCode != 200 {
957+
panic(fmt.Errorf("error response from data plugin: %v, body: %s", resp.Status, string(body)))
958+
}
959+
960+
// The response is base64 encoded
961+
base64EncodedResponse := string(body)
962+
963+
// Decode the base64 response
964+
decodedResponse, err := base64.StdEncoding.DecodeString(base64EncodedResponse)
965+
if err != nil {
966+
panic(fmt.Errorf("error decoding base64 response: %v", err))
967+
}
968+
969+
// Unmarshal the response
970+
response := map[string]sharedtypes.AnsysGPTRetrieverModuleChunk{}
971+
err = json.Unmarshal(decodedResponse, &response)
972+
if err != nil {
973+
panic(fmt.Errorf("error unmarshalling response: %v", err))
974+
}
975+
logging.Log.Debugf(&logging.ContextMap{}, "Received response from retriever module: %v", response)
976+
977+
// Extract the context from the response
978+
context = make([]sharedtypes.AnsysGPTRetrieverModuleChunk, len(response))
979+
for chunkNum, chunk := range response {
980+
// Extract int from chunkNum
981+
_, chunkNumstring, found := strings.Cut(chunkNum, "chunk ")
982+
if !found {
983+
panic(fmt.Errorf("error extracting chunk number from '%v'", chunkNum))
984+
}
985+
chunkNumInt, err := strconv.Atoi(chunkNumstring)
986+
if err != nil {
987+
panic(fmt.Errorf("error converting chunk number to int: %v", err))
988+
}
989+
// Store the chunk in the context slice
990+
context[chunkNumInt-1] = chunk
991+
}
992+
993+
return context
994+
}
995+
900996
// AecPerformLLMFinalRequest performs a final request to LLM
901997
//
902998
// Tags:
@@ -927,7 +1023,8 @@ func AecPerformLLMFinalRequest(systemTemplate string,
9271023
tokenCountModelName string,
9281024
isStream bool,
9291025
userEmail string,
930-
jwtToken string) (message string, stream *chan string) {
1026+
jwtToken string,
1027+
dontSendTokenCount bool) (message string, stream *chan string) {
9311028

9321029
logging.Log.Debugf(&logging.ContextMap{}, "Performing LLM final request")
9331030

@@ -1018,8 +1115,14 @@ func AecPerformLLMFinalRequest(systemTemplate string,
10181115
}
10191116
totalInputTokenCount := previousInputTokenCount + inputTokenCount
10201117

1118+
// check if token count should be sent
1119+
sendTokenCount := false
1120+
if !dontSendTokenCount {
1121+
sendTokenCount = true
1122+
}
1123+
10211124
// Start a goroutine to transfer the data from the response channel to the stream channel.
1022-
go transferDatafromResponseToStreamChannel(&responseChannel, &streamChannel, false, true, tokenCountEndpoint, totalInputTokenCount, previousOutputTokenCount, tokenCountModelName, jwtToken, userEmail, true, contextString)
1125+
go transferDatafromResponseToStreamChannel(&responseChannel, &streamChannel, false, sendTokenCount, tokenCountEndpoint, totalInputTokenCount, previousOutputTokenCount, tokenCountModelName, jwtToken, userEmail, true, contextString)
10231126

10241127
return "", &streamChannel
10251128
}

pkg/externalfunctions/externalfunctions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ var ExternalFunctionsMap = map[string]interface{}{
7878
"AisChangeAcsResponsesByFactor": AisChangeAcsResponsesByFactor,
7979
"AecGetContextFromRetrieverModule": AecGetContextFromRetrieverModule,
8080
"AecPerformLLMFinalRequest": AecPerformLLMFinalRequest,
81+
"GetContextFromDataPlugin": GetContextFromDataPlugin,
8182

8283
// data extraction
8384
"GetGithubFilesToExtract": GetGithubFilesToExtract,
@@ -101,6 +102,7 @@ var ExternalFunctionsMap = map[string]interface{}{
101102
"JsonPath": JsonPath,
102103
"StringConcat": StringConcat,
103104
"StringFormat": StringFormat,
105+
"FluentCodeGenTest": FluentCodeGenTest,
104106

105107
// code generation
106108
"LoadCodeGenerationElements": LoadCodeGenerationElements,
@@ -187,4 +189,7 @@ var ExternalFunctionsMap = map[string]interface{}{
187189

188190
// rhsc
189191
"SetCopilotGenerateRequestJsonBody": SetCopilotGenerateRequestJsonBody,
192+
193+
// fluent
194+
"FluentCodeGen": FluentCodeGen,
190195
}

pkg/externalfunctions/fluent.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
2+
// SPDX-License-Identifier: MIT
3+
//
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
package externalfunctions
24+
25+
import (
26+
"bytes"
27+
"encoding/json"
28+
"fmt"
29+
"io"
30+
"net/http"
31+
"strings"
32+
)
33+
34+
// FluentCodeGen sends a raw user message to the Fluent container and returns the response
35+
//
36+
// Tags:
37+
// - @displayName: Fluent Code Gen
38+
//
39+
// Parameters:
40+
// - message: the raw user message to send to the container
41+
//
42+
// Returns:
43+
// - response: the response from the Fluent container as a string
44+
func FluentCodeGen(message string) (response string) {
45+
url := "http://aali-fluent:8000/chat"
46+
47+
// Create the JSON payload directly
48+
jsonData := fmt.Sprintf(`{"message": "%s"}`, message)
49+
50+
// Create HTTP request
51+
req, err := http.NewRequest("POST", url, bytes.NewBufferString(jsonData))
52+
if err != nil {
53+
panic(fmt.Sprintf("Error creating HTTP request: %v", err))
54+
}
55+
56+
// Set headers
57+
req.Header.Set("Accept", "application/json")
58+
req.Header.Set("Content-Type", "application/json")
59+
60+
// Execute the request
61+
client := &http.Client{}
62+
resp, err := client.Do(req)
63+
if err != nil {
64+
panic(fmt.Sprintf("Error executing HTTP request: %v", err))
65+
}
66+
defer resp.Body.Close()
67+
68+
// Read the response body
69+
body, err := io.ReadAll(resp.Body)
70+
if err != nil {
71+
panic(fmt.Sprintf("Error reading response body: %v", err))
72+
}
73+
74+
// Check if the response code is successful (2xx)
75+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
76+
panic(fmt.Sprintf("HTTP request failed with status code %d: %s", resp.StatusCode, string(body)))
77+
}
78+
79+
// Parse JSON response to extract just the response content
80+
var responseData map[string]interface{}
81+
if err := json.Unmarshal(body, &responseData); err != nil {
82+
panic(fmt.Sprintf("Error parsing JSON response: %v", err))
83+
}
84+
85+
// Extract the response field
86+
if responseField, exists := responseData["response"]; exists {
87+
if responseArray, ok := responseField.([]interface{}); ok && len(responseArray) > 0 {
88+
// Concatenate all items in the response array with a newline
89+
var concatenatedResponse string
90+
for _, item := range responseArray {
91+
if str, ok := item.(string); ok {
92+
concatenatedResponse += str + "\n"
93+
} else {
94+
concatenatedResponse += fmt.Sprintf("%v\n", item)
95+
}
96+
}
97+
// Remove the trailing newline
98+
concatenatedResponse = strings.TrimRight(concatenatedResponse, "\n")
99+
return "```python\n" + concatenatedResponse + "```"
100+
}
101+
}
102+
103+
// Fallback to raw response if parsing fails
104+
return string(body)
105+
}

pkg/externalfunctions/generic.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,77 @@ func StringFormat(data any, format string) string {
270270
}
271271
return fmt.Sprintf(format, data)
272272
}
273+
274+
// FluentCodeGen sends a raw user message to the Fluent container and returns the response.
275+
// It takes a user message and posts it to the Fluent API endpoint to generate code.
276+
//
277+
// Tags:
278+
// - @displayName: Fluent Code Gen Test
279+
//
280+
// Parameters:
281+
// - message: the raw user message to send to the container
282+
//
283+
// Returns:
284+
// - response: the response from the Fluent container as a string
285+
func FluentCodeGenTest(message string) (response string) {
286+
url := "http://aali-fluent:8000/chat"
287+
288+
// Create the JSON payload directly
289+
jsonData := fmt.Sprintf(`{"message": "%s"}`, message)
290+
291+
// Create HTTP request
292+
req, err := http.NewRequest("POST", url, bytes.NewBufferString(jsonData))
293+
if err != nil {
294+
panic(fmt.Sprintf("Error creating HTTP request: %v", err))
295+
}
296+
297+
// Set headers
298+
req.Header.Set("Accept", "application/json")
299+
req.Header.Set("Content-Type", "application/json")
300+
301+
// Execute the request
302+
client := &http.Client{}
303+
resp, err := client.Do(req)
304+
if err != nil {
305+
panic(fmt.Sprintf("Error executing HTTP request: %v", err))
306+
}
307+
defer resp.Body.Close()
308+
309+
// Read the response body
310+
body, err := io.ReadAll(resp.Body)
311+
if err != nil {
312+
panic(fmt.Sprintf("Error reading response body: %v", err))
313+
}
314+
315+
// Check if the response code is successful (2xx)
316+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
317+
panic(fmt.Sprintf("HTTP request failed with status code %d: %s", resp.StatusCode, string(body)))
318+
}
319+
320+
// Parse JSON response to extract just the response content
321+
var responseData map[string]interface{}
322+
if err := json.Unmarshal(body, &responseData); err != nil {
323+
panic(fmt.Sprintf("Error parsing JSON response: %v", err))
324+
}
325+
326+
// Extract the response field
327+
if responseField, exists := responseData["response"]; exists {
328+
if responseArray, ok := responseField.([]interface{}); ok && len(responseArray) > 0 {
329+
// Concatenate all items in the response array with a newline
330+
var concatenatedResponse string
331+
for _, item := range responseArray {
332+
if str, ok := item.(string); ok {
333+
concatenatedResponse += str + "\n"
334+
} else {
335+
concatenatedResponse += fmt.Sprintf("%v\n", item)
336+
}
337+
}
338+
// Remove the trailing newline
339+
concatenatedResponse = strings.TrimRight(concatenatedResponse, "\n")
340+
return "```python\n" + concatenatedResponse + "```"
341+
}
342+
}
343+
344+
// Fallback to raw response if parsing fails
345+
return string(body)
346+
}

0 commit comments

Comments
 (0)