Skip to content
Merged
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
2 changes: 1 addition & 1 deletion workspaces/agent-forge/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions workspaces/agent-forge/plugins/agent-forge/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ export interface Config {
*/
useOpenIDToken?: boolean;

/**
* Whether to enable feedback (default: false)
* @visibility frontend
*/
enableFeedback?: boolean;

/**
* The endpoint for submitting feedback (default: /submit_feedback)
* @visibility frontend
*/
feedbackEndpoint?: string;

/**
* The header title to display (default: bot name)
* @visibility frontend
Expand Down
2 changes: 1 addition & 1 deletion workspaces/agent-forge/plugins/agent-forge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@caipe/plugin-agent-forge",
"version": "0.3.39",
"version": "0.3.40",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ import {
AgentCard,
} from '../a2a/schema';
import { IdentityApi, OpenIdConnectApi } from '@backstage/core-plugin-api';
import { Message, Feedback } from '../types';
import { createTimestamp } from '../utils';
import axios, { AxiosError } from 'axios';

export interface IChatbotApiOptions {
requestTimeout?: number;
useOpenIDToken?: boolean;
feedbackEndpoint?: string | null;
}

export class ChatbotApi {
Expand All @@ -35,9 +39,13 @@ export class ChatbotApi {
private identityApi: IdentityApi;
private openIdConnectApi: OpenIdConnectApi | null;
private useOpenIDToken: boolean;
private feedbackEndpoint: string | null;
constructor(
private apiBaseUrl: string,
options: { identityApi: IdentityApi; openIdConnectApi?: OpenIdConnectApi | null },
options: {
identityApi: IdentityApi;
openIdConnectApi?: OpenIdConnectApi | null;
},
apiOptions?: IChatbotApiOptions,
) {
this.contextId = '';
Expand All @@ -47,6 +55,7 @@ export class ChatbotApi {
this.identityApi = options.identityApi;
this.openIdConnectApi = options.openIdConnectApi ?? null;
this.useOpenIDToken = apiOptions?.useOpenIDToken ?? false; // default to false which means use IdentityApi.getCredentials() (backstage token)
this.feedbackEndpoint = apiOptions?.feedbackEndpoint ?? null;
try {
const timeout = apiOptions?.requestTimeout ?? 300; // Default to 300 seconds
this.client = new A2AClient(this.apiBaseUrl, timeout);
Expand All @@ -58,7 +67,9 @@ export class ChatbotApi {
private async getToken(): Promise<string | undefined> {
if (this.useOpenIDToken) {
if (!this.openIdConnectApi) {
console.warn('useOpenIDToken is true but openIdConnectApi is not provided, falling back to IdentityApi');
console.warn(
'useOpenIDToken is true but openIdConnectApi is not provided, falling back to IdentityApi',
);
const credentials = await this.identityApi.getCredentials();
return credentials.token;
}
Expand Down Expand Up @@ -158,6 +169,52 @@ export class ChatbotApi {
}
}

public async submitFeedback(
message: Message,
feedback: Feedback,
): Promise<void> {
if (!this.feedbackEndpoint) {
throw new Error('Feedback endpoint is not configured');
}

try {
// always use backstage token for Jarvis
// TODO: maybe this should be configurable
const { token } = await this.identityApi.getCredentials();

// Submit feedback without Authorization header to avoid CORS preflight
const { status } = await axios.post(
this.feedbackEndpoint,
{
type: feedback.type,
reason: feedback.reason,
additionalFeedback: feedback.additionalFeedback || '',
timestamp: createTimestamp(),
message: message.text,
},
{
headers: {
Authorization: `Bearer ${token}`, // Commented out to avoid CORS preflight
'Content-Type': 'application/json',
},
},
);
if (status !== 200) {
throw new Error('Failed to submit feedback');
}
} catch (error) {
const err = error as AxiosError;
if (err?.isAxiosError) {
throw new Error(
`Error submitting feedback: ${[err.message, err.cause?.message]
.filter(Boolean)
.join(' - ')}`,
);
}
throw new Error(err.message);
}
}

public async cancelTask(taskId: string): Promise<void> {
try {
if (!this.client) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import {
DEFAULT_SUGGESTIONS,
DEFAULT_THINKING_MESSAGES,
} from '../constants';
import { Message } from '../types';
import { Message, Feedback } from '../types';
import { ChatSession, ChatStorage } from '../types/chat';
import { createTimestamp } from '../utils';
import { ChatContainer } from './ChatContainer';
Expand Down Expand Up @@ -293,10 +293,13 @@ export function AgentForgePage() {
const backendUrl =
config.getOptionalString('agentForge.baseUrl') ||
config.getString('backend.baseUrl');
const authApiId =
config.getOptionalString('agentForge.authApiId'); // Optional - only needed if using a custom auth provider
const authApiId = config.getOptionalString('agentForge.authApiId'); // Optional - only needed if using a custom auth provider
const useOpenIDToken =
config.getOptionalBoolean('agentForge.useOpenIDToken') ?? false;
const enableFeedback =
config.getOptionalBoolean('agentForge.enableFeedback') ?? false;
const feedbackEndpoint =
config.getOptionalString('agentForge.feedbackEndpoint') ?? null;
const requestTimeout =
config.getOptionalNumber('agentForge.requestTimeout') || 300;
const enableStreaming =
Expand Down Expand Up @@ -340,10 +343,15 @@ export function AgentForgePage() {
// OpenIdConnectApiRef - only create if authApiId is provided
const OpenIdConnectApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> | null = authApiId ? createApiRef({
id: authApiId,
}) : null;
const openIdConnectApi = OpenIdConnectApiRef ? useApi(OpenIdConnectApiRef) : null;
> | null = authApiId
? createApiRef({
id: authApiId,
})
: null;
// eslint-disable-next-line react-hooks/rules-of-hooks
const openIdConnectApi = OpenIdConnectApiRef
? useApi(OpenIdConnectApiRef)
: null;

// Create initial session factory
const createInitialSession = useCallback(
Expand Down Expand Up @@ -405,6 +413,7 @@ export function AgentForgePage() {
const [suggestions, setSuggestions] = useState<string[]>(initialSuggestions);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [feedback, setFeedback] = useState<{ [key: number]: Feedback }>({});
const [connectionStatus, setConnectionStatus] = useState<
'checking' | 'connected' | 'disconnected'
>('checking');
Expand Down Expand Up @@ -922,11 +931,14 @@ export function AgentForgePage() {
authApiId,
);

console.log('🔧 Feedback endpoint:', feedbackEndpoint);
console.log('🔧 Feedback enabled:', enableFeedback);

try {
const api = new ChatbotApi(
backendUrl,
{ identityApi, openIdConnectApi },
{ requestTimeout, useOpenIDToken },
{ requestTimeout, useOpenIDToken, feedbackEndpoint },
);

// Wrap API methods to catch any remaining A2A client exceptions
Expand Down Expand Up @@ -3582,6 +3594,73 @@ export function AgentForgePage() {
[currentSessionId, handleMessageSubmit, addMessageToSession],
);

// Feedback handlers
const handleFeedbackChange = useCallback(
(index: number, newFeedback: Feedback) => {
if (!enableFeedback) return;
setFeedback(prev => ({
...prev,
[index]: newFeedback,
}));
},
[enableFeedback],
);

const handleFeedbackSubmit = useCallback(
async (index: number, feedbackData: Feedback) => {
if (!enableFeedback) return;

const session = sessions.find(s => s.contextId === currentSessionId);
if (!session) return;

const message = session.messages[index];
if (!message) return;

try {
await chatbotApi?.submitFeedback(message, feedbackData);
alertApi.post({
severity: 'success',
message: 'Thank you for your feedback!',
});

setFeedback(prev => ({
...prev,
[index]: {
...feedbackData,
submitted: true,
showFeedbackOptions: false,
},
}));

// Update message in session
setSessions(prev =>
prev.map(s => {
if (s.contextId === currentSessionId) {
return {
...s,
messages: s.messages.map((msg, i) => {
if (i === index) {
return { ...msg, showFeedbackOptions: false };
}
return msg;
}),
updatedAt: new Date(),
};
}
return s;
}),
);
} catch (error) {
alertApi.post({
severity: 'error',
message:
'There was an error submitting your feedback. Please try again.',
});
}
},
[sessions, currentSessionId, chatbotApi, alertApi, enableFeedback],
);

const resetChat = () => {
console.log('🔄 Reset chat triggered');
if (currentSessionId) {
Expand Down Expand Up @@ -3802,6 +3881,10 @@ export function AgentForgePage() {
currentOperation={currentOperation}
isInOperationalMode={isInOperationalMode}
onMetadataSubmit={handleMetadataSubmit}
enableFeedback={enableFeedback}
feedback={feedback}
onFeedbackChange={handleFeedbackChange}
onFeedbackSubmit={handleFeedbackSubmit}
fontSizes={{
messageText: fontSizes.messageText,
codeBlock: fontSizes.codeBlock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import React, {
useState,
memo,
} from 'react';
import { Message } from '../types';
import { Message, Feedback } from '../types';
import { ChatMessage } from './ChatMessage';

const useStyles = makeStyles(theme => ({
Expand Down Expand Up @@ -221,6 +221,12 @@ export interface ChatContainerProps {
onSuggestionClick: (suggestion: string) => void;
onMetadataSubmit?: (messageId: string, data: Record<string, any>) => void;

// Feedback props
enableFeedback?: boolean;
feedback?: { [key: number]: Feedback };
onFeedbackChange?: (index: number, feedback: Feedback) => void;
onFeedbackSubmit?: (index: number, feedback: Feedback) => void;

// Scroll-based message loading
onScroll?: (
scrollTop: number,
Expand Down Expand Up @@ -258,6 +264,10 @@ const MessagesList = memo(function MessagesList({
autoExpandExecutionPlans,
executionPlanLoading,
onMetadataSubmit,
enableFeedback,
feedback,
onFeedbackChange,
onFeedbackSubmit,
}: {
messages: Message[];
botName: string;
Expand All @@ -273,6 +283,10 @@ const MessagesList = memo(function MessagesList({
autoExpandExecutionPlans?: Set<string>;
executionPlanLoading?: Set<string>;
onMetadataSubmit?: (messageId: string, data: Record<string, any>) => void;
enableFeedback?: boolean;
feedback?: { [key: number]: Feedback };
onFeedbackChange?: (index: number, feedback: Feedback) => void;
onFeedbackSubmit?: (index: number, feedback: Feedback) => void;
}) {
// Memoize font sizes to prevent re-creating object on every render
const memoizedFontSizes = useMemo(
Expand Down Expand Up @@ -307,6 +321,18 @@ const MessagesList = memo(function MessagesList({
autoExpandExecutionPlans={autoExpandExecutionPlans}
executionPlanLoading={executionPlanLoading}
onMetadataSubmit={onMetadataSubmit}
enableFeedback={enableFeedback}
messageFeedback={feedback?.[index]}
onFeedbackChange={
onFeedbackChange
? newFeedback => onFeedbackChange(index, newFeedback)
: undefined
}
onFeedbackSubmit={
onFeedbackSubmit
? feedbackData => onFeedbackSubmit(index, feedbackData)
: undefined
}
/>
</div>
))}
Expand Down Expand Up @@ -342,6 +368,10 @@ export const ChatContainer = memo(function ChatContainer({
botName,
botIcon,
inputPlaceholder,
enableFeedback = false,
feedback,
onFeedbackChange,
onFeedbackSubmit,
fontSizes,
onMessageSubmit,
onCancelRequest,
Expand Down Expand Up @@ -598,9 +628,14 @@ export const ChatContainer = memo(function ChatContainer({
botIcon={botIcon}
fontSizes={fontSizes}
executionPlanBuffer={executionPlanBuffer}
executionPlanHistory={executionPlanHistory}
autoExpandExecutionPlans={autoExpandExecutionPlans}
executionPlanLoading={executionPlanLoading}
onMetadataSubmit={onMetadataSubmit}
enableFeedback={enableFeedback}
feedback={feedback}
onFeedbackChange={onFeedbackChange}
onFeedbackSubmit={onFeedbackSubmit}
/>

{isTyping && (
Expand Down
Loading
Loading