Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public isolated function getRecommendation(RecommendationRequest payload) return
}

# Get summary for a conversation.
#
#
# + projectId - Project ID
# + conversationId - Conversation ID
# + return - Summary response or error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final http:Client aiChatAgentClient = check new (aiChatAgentBaseUrl, {
});

isolated function createAiChatAgentWsClient(string sessionId) returns websocket:Client|error {
return new (string `${aiChatAgentWsBaseUrl}/ws?sessionId=${sessionId}`, {
return new (string `${aiChatAgentWsBaseUrl}/ws/${sessionId}`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Ballerina HTTP resource function: how to declare a path parameter vs a query parameter in the function signature

💡 Result:

In Ballerina HTTP resource functions, path parameters and query parameters are declared differently in the function signature. Path parameters are declared in the resource path using square brackets with the type, e.g., resource function get users/[string id]. The parameter 'id' of type string is automatically bound from the path segment. Query parameters are declared as regular typed parameters in the function signature without any special annotation (unless resolving ambiguity with payload), e.g., resource function get users(string name?, int limit = 10). They bind from the URL query string ?name=foo&limit=20. They can be optional (? suffix) or have defaults. Example: service / on new http:Listener(9090) { // Path param: matches /users/123 resource function get users/int id returns json { return {"id": id}; } // Query params: matches /users?name=foo&limit=10 resource function get users(string? name, int limit = 10) returns json[] { // use name and limit } }

Citations:


🏁 Script executed:

#!/bin/bash
# Confirm how `sessionId` is declared on the WS upgrade resource.
rg -nP -C2 'resource function get ws' --type=bal
# Also look for any alternate resource paths that capture sessionId as a path segment.
rg -nP --type=bal 'ws/\[string\s+sessionId\]|get\s+ws\b'

Repository: wso2-open-operations/cs-tools

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Locate service.bal file
fd -name "service.bal" apps/customer-portal/backend/

# Search for the WS resource without using --type flag
# Focus on the modules/ai_chat_agent or service.bal at the backend level
rg -nA5 'resource function get ws' apps/customer-portal/backend/ 2>/dev/null || true

# Also check the client.bal file to see the full context of line 43
echo "=== Client code context ==="
fd -name "client.bal" apps/customer-portal/backend/modules/ai_chat_agent/ -x cat -n {}

Repository: wso2-open-operations/cs-tools

Length of output: 1247


🏁 Script executed:

#!/bin/bash
# Read the client.bal file directly to see line 43 context
cat apps/customer-portal/backend/modules/ai_chat_agent/client.bal

Repository: wso2-open-operations/cs-tools

Length of output: 1727


Path/query parameter mismatch: WebSocket upgrade will fail.

The client now builds /ws/${sessionId} as a path segment, but the server resource at service.bal:4597 is declared as:

isolated resource function get ws(http:Request req, string sessionId) returns websocket:Service|websocket:UpgradeError {

In Ballerina, a parameter is a path parameter only when it appears in the resource path brackets (e.g., get ws/[string sessionId]). Since sessionId is a plain function parameter here, Ballerina treats it as a query parameter. Requests to /ws/<id> will not match the /ws resource and the WebSocket upgrade will return 404, breaking the chat flow.

Fix either the client (use query-param form: /ws?sessionId=${sessionId}) or the server resource path (add path segment):

🔧 Suggested server-side fix (service.bal ~line 4597)
-    isolated resource function get ws(http:Request req, string sessionId) returns websocket:Service|websocket:UpgradeError {
+    isolated resource function get ws/[string sessionId](http:Request req) returns websocket:Service|websocket:UpgradeError {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/backend/modules/ai_chat_agent/client.bal` at line 43,
The client builds the WebSocket URL as a path segment which won't match the
server resource signature; update the WebSocket URL construction in the return
new(...) expression to send sessionId as a query parameter instead (use
aiChatAgentWsBaseUrl and sessionId to produce /ws?sessionId=<id>) so the server
resource function get ws(http:Request req, string sessionId) receives sessionId
correctly.

auth: {
...clientCredentialsOauth2ConfigWs
}
Expand Down
6 changes: 6 additions & 0 deletions apps/customer-portal/backend/modules/types/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,12 @@ public type Conversation record {|
ReferenceItem? case;
# State information
ReferenceItem? state;
# Total messages exchanged in the chatbot conversation
int messagesExchanged?;
# Number of troubleshooting tool invocations in the chatbot conversation
int troubleshootingAttempts?;
# Number of unique KB articles surfaced in the chatbot conversation
int kbArticlesReviewed?;
json...;
|};

Expand Down
34 changes: 32 additions & 2 deletions apps/customer-portal/backend/service.bal
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
log:printInfo("Customer Portal backend started.");
}

# Fetch metadata information for the customer portal.
#
# + return - Metadata information or error response
resource function get metadata(http:RequestContext ctx)
Expand Down Expand Up @@ -1370,8 +1369,39 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
};
}

types:ConversationSearchResponse baseResponse = mapConversationSearchResponse(conversationResponse);

// Fan-out: fire all summary calls in parallel to avoid N serial round-trips.
future<ai_chat_agent:ConversationSummaryResponse|error>[] summaryFutures = [];
foreach types:Conversation conv in baseResponse.conversations {
future<ai_chat_agent:ConversationSummaryResponse|error> f = start ai_chat_agent:getSummary(id, conv.id);
summaryFutures.push(f);
}

// Fan-in: collect results and enrich each conversation.
types:Conversation[] enrichedConversations = [];
foreach int i in 0 ..< baseResponse.conversations.length() {
types:Conversation enriched = baseResponse.conversations[i].clone();
ai_chat_agent:ConversationSummaryResponse|error summary = wait summaryFutures[i];
if summary is ai_chat_agent:ConversationSummaryResponse {
enriched.messagesExchanged = summary.messagesExchanged;
enriched.troubleshootingAttempts = summary.troubleshootingAttempts;
enriched.kbArticlesReviewed = summary.kbArticlesReviewed;
} else {
enriched.messagesExchanged = 0;
enriched.troubleshootingAttempts = 0;
enriched.kbArticlesReviewed = 0;
}
enrichedConversations.push(enriched);
}

return <http:Ok>{
body: mapConversationSearchResponse(conversationResponse)
body: {
conversations: enrichedConversations,
totalRecords: baseResponse.totalRecords,
'limit: baseResponse.'limit,
offset: baseResponse.offset
}
};
}

Expand Down