Skip to content

[BUG][Chat] Unregister tools when component unmount#10999

Merged
ananzh merged 2 commits intoopensearch-project:mainfrom
ananzh:chat-fix-unregister-tool
Dec 3, 2025
Merged

[BUG][Chat] Unregister tools when component unmount#10999
ananzh merged 2 commits intoopensearch-project:mainfrom
ananzh:chat-fix-unregister-tool

Conversation

@ananzh
Copy link
Copy Markdown
Member

@ananzh ananzh commented Dec 1, 2025

Description

Note: Need #10983 to merge first

This PR allows to tools to be unregistered when component unmount. So when navigate from one page to another, the page tool will be unregistered and can't be included to tools to AG-UI.

Issues Resolved

NA

Screenshot

Screenshot 2025-12-01 at 1 44 07 PM

Changelog

  • fix: [BUG][Chat] Unregister tools when component unmount

Check List

  • All tests pass
    • yarn test:jest
    • yarn test:jest_integration
  • New functionality includes testing.
  • New functionality has been documented.
  • Update CHANGELOG.md
  • Commits are signed per the DCO using --signoff

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where tools were not properly cleaned up when the Chat component unmounted, ensuring proper resource management.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 1, 2025

Walkthrough

Introduces a core Chat Service architecture by extracting chat functionality into a managed CoreService with lifecycle control, thread/window state management, and plugin delegation. Integrates chat throughout the core system and refactors existing chat and explore plugins to use the new service. Removes legacy graph visualization error handling and adds ML agent routing infrastructure.

Changes

Cohort / File(s) Summary
Core Chat Service
src/core/public/chat/types.ts, src/core/public/chat/chat_service.ts, src/core/public/chat/chat_service.mock.ts, src/core/public/chat/chat_service.test.ts, src/core/public/chat/index.ts
Implements new ChatService class with setup/start/stop lifecycle, thread/window state management, and delegated messaging. Includes TypeScript types for messages, window state, and service contracts (ChatServiceInterface, ChatServiceSetup, ChatServiceStart, ChatImplementationFunctions). Provides comprehensive test suite and mock factories for integration testing.
Core System Integration
src/core/public/core_system.ts, src/core/public/index.ts, src/core/public/mocks.ts, src/core/public/plugins/plugin_context.ts, src/core/public/plugins/plugins_service.test.ts
Wires ChatService into CoreSystem lifecycle, exposes chat through CoreSetup/CoreStart contracts, updates plugin context to include chat dependency, and extends core mocks with chat integration.
Chat Plugin Core
src/plugins/chat/common/chat_capabilities.ts, src/plugins/chat/common/chat_capabilities.test.ts, src/plugins/chat/common/types.ts
Adds unified chat enablement logic via isChatEnabled and hasInvestigationCapabilities functions. Re-exports core chat types and introduces ChatConfig/ContextProviderConfig interfaces.
Chat Plugin Services
src/plugins/chat/public/services/chat_service.ts, src/plugins/chat/public/services/chat_service.test.ts, src/plugins/chat/public/services/chat_event_handler.ts, src/plugins/chat/public/services/chat_event_handler.test.ts
Refactors ChatService to accept coreChatService parameter, delegates thread/window state to core service, integrates workspace-aware data source selection, and updates tool result handling. Removes ChatContextManager entirely.
Chat Plugin UI
src/plugins/chat/public/plugin.ts, src/plugins/chat/public/plugin.test.ts, src/plugins/chat/public/components/chat_header_button.tsx, src/plugins/chat/public/components/chat_header_button.test.tsx, src/plugins/chat/public/components/chat_window.tsx, src/plugins/chat/public/types.ts
Updates plugin setup to accept CoreSetup, wires implementation to core chat service via setImplementation, registers chat in header controls. Refactors ChatWindow to remove local tool state and console grouping. Updates types to include DataSourceManagement.
Graph Visualization Cleanup
src/plugins/chat/public/components/graph_visualization/error_boundary.tsx, src/plugins/chat/public/components/graph_visualization/error_display.tsx, src/plugins/chat/public/components/graph_visualization/error_handler.ts, src/plugins/chat/public/components/graph_visualization/validation.ts
Removes entire error boundary system, error display components, and error handler utilities. Deletes comprehensive error modeling and recovery logic previously used for chart error handling.
Server-Side ML Routing
src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts, src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts, src/plugins/chat/server/routes/ml_routes/router_registry.ts, src/plugins/chat/server/routes/ml_routes/ml_commons_agent.ts, src/plugins/chat/server/routes/ml_routes/ml_commons_agent.test.ts
Introduces extensible ML agent routing with MLAgentRouter interface and GenericMLRouter implementation. Adds MLAgentRouterFactory and MLAgentRouterRegistry for dynamic router registration. Removes legacy forwardToMLCommonsAgent function and tests.
Chat Proxy Route
src/plugins/chat/server/routes/index.ts, src/plugins/chat/server/routes/index.test.ts, src/plugins/chat/server/index.ts
Refactors /api/chat/proxy to support dataSourceId query parameter, implements dynamic routing via ML agent routers with fallback to AG-UI. Adds capabilities resolver integration and forwardToAgUI helper.
Chat Plugin Manifest
src/plugins/chat/opensearch_dashboards.json
Adds dataSourceManagement to optionalPlugins and requiredBundles.
Explore Plugin Updates
src/plugins/explore/public/actions/ask_ai_action.ts, src/plugins/explore/public/actions/ask_ai_action.test.ts, src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.tsx, src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.test.tsx, src/plugins/explore/public/components/tabs/action_bar/action_bar.tsx, src/plugins/explore/public/build_services.ts, src/plugins/explore/public/plugin.ts, src/plugins/explore/public/types.ts, src/plugins/explore/opensearch_dashboards.json
Migrates from ChatService to ChatServiceStart, uses core.chat instead of plugins.chat, delegates enablement to action's isCompatible via isAvailable(). Removes chat from optionalPlugins. Updates function signatures for slotRegistry optionality.
Data Source Management
src/plugins/data_source_management/public/index.ts
Re-exports getWorkspaces utility.
Changelog
changelogs/fragments/10983.yml, changelogs/fragments/10999.yml
Documents chat architecture refactoring and tool unregistration on Chat component unmount.

Sequence Diagram

sequenceDiagram
    participant Plugin as Chat Plugin
    participant Core as CoreSystem
    participant ChatSvc as ChatService (Core)
    participant AppImpl as ChatImplementation (Plugin)
    participant UI as UI/Components

    Note over Core,ChatSvc: Initialization Phase
    Core->>ChatSvc: new ChatService()
    ChatSvc->>ChatSvc: Initialize threadId$, windowState$
    
    Core->>ChatSvc: setup()
    ChatSvc-->>Core: ChatServiceSetup { setImplementation, ... }
    Core-->>Plugin: expose in CoreSetup

    Plugin->>Plugin: initialize ChatService impl
    Plugin->>Core: setup(core)
    Plugin->>ChatSvc: setImplementation(chatImpl)
    
    Note over Core,UI: Start Phase
    Core->>ChatSvc: start()
    ChatSvc-->>Core: ChatServiceStart { isAvailable, sendMessage, ... }
    Core-->>Plugin: expose in CoreStart
    
    Plugin->>ChatSvc: ChatService(uiSettings, coreChatService)
    
    Note over ChatSvc,UI: Message Send Flow
    UI->>Plugin: sendMessage(content)
    Plugin->>ChatSvc: sendMessage(content)
    ChatSvc->>ChatSvc: validate availability via impl
    ChatSvc->>AppImpl: delegated to implementation
    AppImpl-->>ChatSvc: Observable<Message>
    ChatSvc-->>Plugin: Observable streams
    Plugin-->>UI: message updates
    
    Note over ChatSvc,AppImpl: Window Management
    UI->>ChatSvc: openWindow()
    ChatSvc->>AppImpl: delegated openWindow
    AppImpl-->>ChatSvc: success
    ChatSvc->>ChatSvc: updateWindowState$
    ChatSvc->>UI: notify onWindowOpen callbacks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • src/core/public/chat/chat_service.ts — Dense service logic with lifecycle management, state synchronization via BehaviorSubjects, callback registries, and delegation patterns; verify thread ID generation, window state consistency, and error handling for unavailable implementations.
  • src/plugins/chat/public/services/chat_service.ts — Complex refactoring replacing local state with core service delegation, workspace-aware dataSourceId resolution, AssistantActionService subscription integration, and streaming state management; ensure all message flows propagate dataSourceId correctly and lifecycle cleanup is complete.
  • src/plugins/chat/server/routes/index.ts — Orchestrates dynamic ML routing with capabilities-based fallback to AG-UI, introduces forwardToAgUI helper and dataSourceId threading; verify routing decision logic, error handling paths, and response streaming correctness.
  • src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.tsx — Changes from ChatService to ChatServiceStart and simplifies context handling; confirm message dispatch and window handling work correctly with new interface.
  • src/plugins/chat/public/components/chat_window.tsx — Large refactoring removing tool state management, eliminating console grouping, adding lifecycle/sync effects, and changing event handler dependencies; verify event routing, message timeline consistency, and resource cleanup are correct.

Poem

🐰 A Chat Service hops into Core's embrace,
States managed gracefully, in proper place.
Window and threads dance with ease,
ML routers flow, with data source keys.
Error boundaries fade to the past—
Clean architecture built to last! 🎩✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive PR description covers the basic intent (unregistering tools on unmount) and includes a changelog entry, but is incomplete against the template requirements. Clarify the technical approach taken, confirm all checklist items completion (especially testing and DCO signoff), and provide more detailed testing steps beyond the screenshot.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title '[BUG][Chat] Unregister tools when component unmount' clearly summarizes the main change: fixing a bug where tools are not unregistered when the chat component unmounts.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

opensearch-changeset-bot bot added a commit to ananzh/OpenSearch-Dashboards that referenced this pull request Dec 1, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugins/chat/public/services/chat_service.ts (1)

446-457: saveCurrentChatState calls this.getThreadId() which may throw.

If the core chat service is unavailable when saveCurrentChatState is called (e.g., during early initialization or cleanup), this will throw an error. Consider adding a guard.

 private saveCurrentChatState(): void {
+  if (!this.coreChatService) {
+    console.warn('Cannot save chat state: core chat service not available');
+    return;
+  }
   const state: CurrentChatState = {
     threadId: this.getThreadId(),
     messages: this.currentMessages,
   };
🧹 Nitpick comments (23)
src/plugins/chat/public/actions/graph_timeseries_data_action.tsx (1)

42-42: LGTM! Clean API extension with backward compatibility.

The addition of the enabled parameter with a default value of true maintains backward compatibility while enabling runtime control of action registration.

Optionally, consider adding JSDoc documentation to clarify the parameter's purpose:

+/**
+ * Registers the graph timeseries data action with the assistant.
+ * @param enabled - Controls whether the action is enabled (default: true)
+ */
 export function useGraphTimeseriesDataAction(enabled: boolean = true) {
src/plugins/chat/common/types.ts (1)

8-20: Core chat type re-exports are a clean consolidation

Re-exporting ToolCall/FunctionCall/Message/Role from ../../../core/public/chat centralizes the structural definitions while keeping local Zod schemas for runtime validation. This reduces duplication of interface declarations.

src/plugins/chat/public/services/ag_ui_agent.ts (1)

31-53: Optional dataSourceId support is good; consider more robust URL building

Extending runAgent with an optional dataSourceId and threading it via a query parameter is backward compatible and solves the routing need.

To make this future‑proof if proxyUrl ever includes its own query string, consider using URL/URLSearchParams or at least switching between ? and & based on the presence of ?:

-      const url = dataSourceId
-        ? `${this.proxyUrl}?dataSourceId=${encodeURIComponent(dataSourceId)}`
-        : this.proxyUrl;
+      const url = (() => {
+        if (!dataSourceId) return this.proxyUrl;
+        const separator = this.proxyUrl.includes('?') ? '&' : '?';
+        return `${this.proxyUrl}${separator}dataSourceId=${encodeURIComponent(dataSourceId)}`;
+      })();
src/plugins/explore/public/actions/ask_ai_action.test.ts (1)

15-35: Avoid hand‑rolled ChatServiceStart mocks; reuse the shared core mock

The locally constructed jest.Mocked<ChatServiceStart> only covers part of the ChatServiceStart surface and will easily drift as the core chat API evolves. We already have coreChatServiceMock.createStartContract() providing a fully‑typed, up‑to‑date mock.

Consider using the shared mock instead, and just overwrite isAvailable for specific scenarios, e.g.:

const mockChatService = coreChatServiceMock.createStartContract();

beforeEach(() => {
  jest.clearAllMocks();
  mockChatService.isAvailable.mockReturnValue(true);
});

it('should return false when chatService is not available', () => {
  mockChatService.isAvailable.mockReturnValue(false);
  const action = createAskAiAction(mockChatService);
  expect(action.isCompatible(mockContext)).toBe(false);
});

This keeps tests aligned with the real ChatServiceStart contract and avoids missing fields like future window/thread APIs.

Also applies to: 56-68, 72-91

src/core/public/chat/chat_service.mock.ts (1)

15-48: Align mock observables with real ChatService behavior

getThreadId$ and getWindowState$ currently create a new BehaviorSubject on every invocation. The real ChatService exposes a single subject per instance, and repeated calls to get*$( ) return observables backed by the same internal subject.

To better mirror production behavior (and avoid surprising tests that expect shared state), consider creating the subjects once per mock instance and reusing them:

-import { BehaviorSubject } from 'rxjs';
-import { ChatServiceSetup, ChatServiceStart } from './types';
+import { BehaviorSubject } from 'rxjs';
+import { ChatServiceSetup, ChatServiceStart, ChatWindowState } from './types';

 const createStartContractMock = (): jest.Mocked<ChatServiceStart> => ({
-  isAvailable: jest.fn().mockReturnValue(false),
-  isWindowOpen: jest.fn().mockReturnValue(false),
-  getThreadId$: jest.fn().mockReturnValue(new BehaviorSubject<string>('')),
-  getThreadId: jest.fn().mockReturnValue(''),
+  const threadId$ = new BehaviorSubject<string>('');
+  const windowState$ = new BehaviorSubject<ChatWindowState>({
+    isWindowOpen: false,
+    windowMode: 'sidecar',
+    paddingSize: 400,
+  });
+
+  return {
+    isAvailable: jest.fn().mockReturnValue(false),
+    isWindowOpen: jest.fn().mockReturnValue(false),
+    getThreadId$: jest.fn().mockReturnValue(threadId$.asObservable()),
+    getThreadId: jest.fn().mockReturnValue(''),
   setThreadId: jest.fn(),
   newThread: jest.fn(),
   openWindow: jest.fn(),
   closeWindow: jest.fn(),
-  getWindowState: jest.fn().mockReturnValue({
-    isWindowOpen: false,
-    windowMode: 'sidecar',
-    paddingSize: 400,
-  }),
-  setWindowState: jest.fn(),
+  getWindowState: jest.fn().mockReturnValue(windowState$.getValue()),
+  setWindowState: jest.fn(),
   // ...
-  getWindowState$: jest.fn().mockReturnValue(
-    new BehaviorSubject({
-      isWindowOpen: false,
-      windowMode: 'sidecar',
-      paddingSize: 400,
-    })
-  ),
+  getWindowState$: jest.fn().mockReturnValue(windowState$.asObservable()),
   // ...
-  suggestedActionsService: undefined,
-});
+  suggestedActionsService: undefined,
+});

This keeps the mock semantically closer to the real service while still being easy to override in tests.

src/core/public/chat/chat_service.ts (1)

39-51: Make suggestedActionsService on setup dynamically reflect later registration

In setup(), the returned suggestedActionsService is a plain property set to the value of this.suggestedActionsService at setup time:

return {
  setImplementation: ...,
  setSuggestedActionsService: ...,
  suggestedActionsService: this.suggestedActionsService,
};

If setSuggestedActionsService is invoked after a consumer has captured this setup contract, that consumer will continue to see the old (likely undefined) snapshot.

To keep setup consumers in sync with later registration, consider exposing a getter (similar to start()):

  public setup(): ChatServiceSetup {
-    return {
-      setImplementation: (implementation: ChatImplementationFunctions) => {
-        this.implementation = implementation;
-      },
-
-      setSuggestedActionsService: (service: { registerProvider(provider: any): void }) => {
-        this.suggestedActionsService = service;
-      },
-
-      suggestedActionsService: this.suggestedActionsService,
-    };
+    const chatServiceInstance = this;
+    return {
+      setImplementation: (implementation: ChatImplementationFunctions) => {
+        this.implementation = implementation;
+      },
+
+      setSuggestedActionsService: (service: { registerProvider(provider: any): void }) => {
+        this.suggestedActionsService = service;
+      },
+
+      get suggestedActionsService() {
+        return chatServiceInstance.suggestedActionsService;
+      },
+    };
   }

That keeps the setup contract aligned with the current service state without changing the public type.

src/core/public/chat/index.ts (1)

1-24: LGTM!

Clean barrel file that consolidates the chat module's public API. The exports are well-organized with types first, then implementation, then mocks.

One minor consideration: exporting coreChatServiceMock from the main barrel couples test utilities with production code. Consider whether a separate ./chat/mocks or ./chat/index.mock.ts entry point would be more appropriate for your project conventions.

src/plugins/chat/server/routes/ml_routes/router_registry.ts (1)

23-34: Clarify "initialized" semantics—flag is set even without router registration.

The initialized flag is set to true regardless of whether a router was actually registered (i.e., when hasInvestigationCapabilities returns false). This prevents re-initialization but may be misleading since "initialized" doesn't guarantee a router is available.

Consider whether isInitialized() accurately conveys the intended meaning, or if a separate hasRouter() method would be useful for callers needing to check router availability.

src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1)

56-58: registerRouter silently replaces any existing router.

If a router is already registered, registerRouter will overwrite it without warning. This may be intentional for simplicity, but consider whether:

  1. A warning log should be emitted when replacing an existing router
  2. An error should be thrown to prevent accidental overwrites

If the current behavior is intentional (e.g., for test substitution), a brief comment would clarify this design choice.

  static registerRouter(router: MLAgentRouter): void {
+   // Note: Overwrites any previously registered router. This is intentional
+   // to allow router replacement in tests or reconfiguration scenarios.
    this.router = router;
  }
src/plugins/chat/server/routes/index.test.ts (3)

251-294: Consider adding MLAgentRouterRegistry.reset() in test teardown.

Based on the relevant code snippets, MLAgentRouterRegistry has an initialized flag that persists. Since MLAgentRouterRegistry.initialize() is called within the route handler per request, and tests run sequentially, state may leak between tests if the registry was initialized in a prior test.

Add cleanup in afterEach:

  afterEach(async () => {
    if (server) {
      await server.stop();
    }
+   // Reset ML router registry to prevent state leakage between tests
+   const { MLAgentRouterRegistry } = await import('./ml_routes/router_registry');
+   MLAgentRouterRegistry.reset();
  });

296-339: Test case duplicates logic from previous test.

The test "should fallback to AG-UI when agenticFeaturesEnabled is true but ML client is disabled" has the same setup and assertions as the prior test. The comment says "ML client disabled via missing context ML client" but there's no actual difference in the mock configuration.

Consider either differentiating the test setup or consolidating these tests.


378-403: Test title vs. behavior mismatch.

The test title says "should return 503 when ML Commons agent ID is not configured" but the actual expectation is for the general "No AI agent available" message which occurs because neither ML router nor AG-UI URL is available. The mlCommonsAgentId being undefined doesn't directly cause the 503—it's the combination of no ML router and no AG-UI URL.

Consider updating the test title for clarity:

-      it('should return 503 when ML Commons agent ID is not configured', async () => {
+      it('should return 503 when neither ML router nor AG-UI URL is available', async () => {
src/plugins/chat/common/chat_capabilities.ts (1)

26-28: Consider typing the capabilities parameter.

Using any type loses type safety. If a Capabilities type exists in the codebase (it's imported in routes/index.ts), consider using it here for consistency.

+import { Capabilities } from '../../../../core/server';
+
-export function hasInvestigationCapabilities(capabilities?: any): boolean {
+export function hasInvestigationCapabilities(capabilities?: Capabilities): boolean {
   return capabilities?.investigation?.agenticFeaturesEnabled === true;
 }
src/plugins/chat/server/routes/index.ts (1)

51-66: Consider handling null response body defensively.

Line 52 uses a non-null assertion (agUiResponse.body!). While the body should exist after a successful fetch with ok: true, some edge cases (e.g., certain proxies or unusual responses) could result in a null body.

+  if (!agUiResponse.body) {
+    return response.customError({
+      statusCode: 502,
+      body: { message: 'AG-UI server returned empty response body' },
+    });
+  }
+
   // Convert Web ReadableStream to Node.js Readable stream
-  const reader = agUiResponse.body!.getReader();
+  const reader = agUiResponse.body.getReader();
src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts (3)

12-12: Remove unused import.

Capabilities is imported but never used in this file.

 import {
   Logger,
   RequestHandlerContext,
   OpenSearchDashboardsRequest,
   IOpenSearchDashboardsResponse,
   OpenSearchDashboardsResponseFactory,
-  Capabilities,
 } from '../../../../../core/server';

155-160: Fragile error detection via string matching.

Checking error.message.includes('404') is unreliable—error messages can vary by locale, library version, or upstream changes. Consider checking the error's statusCode property if available, or wrapping the ML client call to capture HTTP status codes directly.

-      if (error instanceof Error && error.message.includes('404')) {
+      // Check for statusCode property if the error exposes it
+      const statusCode = (error as any)?.statusCode ?? (error as any)?.status;
+      if (statusCode === 404) {
         return response.customError({
           statusCode: 404,
           body: { message: `ML Commons agent "${configuredAgentId}" not found` },
         });
       }

149-149: Wrap JSON.parse in try-catch to handle malformed responses.

If mlResponse.body is not valid JSON, JSON.parse will throw an uncaught exception, resulting in a 500 error with no useful message.

-          body: typeof mlResponse.body === 'string' ? JSON.parse(mlResponse.body) : mlResponse.body,
+          body: (() => {
+            if (typeof mlResponse.body !== 'string') return mlResponse.body;
+            try {
+              return JSON.parse(mlResponse.body);
+            } catch {
+              return { rawBody: mlResponse.body };
+            }
+          })(),
src/plugins/chat/public/components/chat_window.tsx (1)

87-96: Direct property mutation on chatService using type assertion.

The pattern (chatService as any).availableTools = state.toolDefinitions bypasses TypeScript's type system. Consider whether availableTools should be part of the public ChatService interface, or if there's a setter method that should be used instead.

src/core/public/chat/types.ts (3)

71-77: Consider extending BaseMessage for consistency.

ToolMessage doesn't extend BaseMessage, which breaks the pattern. While ToolMessage has toolCallId instead of name, it could still extend BaseMessage with role: 'tool' override.

-export interface ToolMessage {
+export interface ToolMessage extends BaseMessage {
   id: string;
   content: string;
   role: 'tool';
   toolCallId: string;
   error?: string;
 }

135-143: Consider using a more specific type instead of any for observable.

The sendMessage and sendMessageWithWindow methods return observable: any. Using a typed observable (e.g., Observable<BaseEvent>) would improve type safety and IDE support.

+import { BaseEvent } from './ag_ui_agent'; // or appropriate import path
+
   sendMessage(
     content: string,
     messages: Message[]
-  ): Promise<{ observable: any; userMessage: UserMessage }>;
+  ): Promise<{ observable: Observable<BaseEvent>; userMessage: UserMessage }>;
   sendMessageWithWindow(
     content: string,
     messages: Message[],
     options?: { clearConversation?: boolean }
-  ): Promise<{ observable: any; userMessage: UserMessage }>;
+  ): Promise<{ observable: Observable<BaseEvent>; userMessage: UserMessage }>;

181-189: Use specific types instead of any for provider registration.

The suggestedActionsService uses any for the provider parameter. Consider defining a proper SuggestedActionsProvider interface to enforce type safety at plugin boundaries.

+export interface SuggestedActionsProvider {
+  // Define the provider interface based on actual requirements
+}
+
 setSuggestedActionsService(service: { registerProvider(provider: any): void }): void;
+// Consider:
+// setSuggestedActionsService(service: { registerProvider(provider: SuggestedActionsProvider): void }): void;
src/plugins/chat/public/services/chat_service.test.ts (1)

87-98: Mock openWindow/closeWindow should update state for consistency.

The mock implementations trigger callbacks but don't update isWindowOpen state. This could cause tests to pass even when real implementation behavior differs.

       openWindow: jest.fn(() => {
         const currentState = mockWindowState$.getValue();
         if (!currentState.isWindowOpen) {
+          mockWindowState$.next({ ...currentState, isWindowOpen: true });
           mockWindowOpenCallbacks.forEach((callback) => callback());
         }
       }),
       closeWindow: jest.fn(() => {
         const currentState = mockWindowState$.getValue();
         if (currentState.isWindowOpen) {
+          mockWindowState$.next({ ...currentState, isWindowOpen: false });
           mockWindowCloseCallbacks.forEach((callback) => callback());
         }
       }),
src/plugins/chat/public/services/chat_service.ts (1)

77-89: Consider graceful degradation instead of throwing errors.

Multiple methods throw Error('Core chat service not available') when coreChatService is undefined. Since these are public methods that could be called early in the lifecycle, consider returning sensible defaults or using optional chaining where appropriate.

For read-only accessors, a fallback might be safer:

 public getThreadId = () => {
   if (!this.coreChatService) {
-    throw new Error('Core chat service not available');
+    console.warn('Core chat service not available, returning empty thread ID');
+    return '';
   }
   return this.coreChatService.getThreadId();
 };

Alternatively, document the precondition that core service must be available, or make the dependency required in the constructor.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7038a73 and b301383.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (54)
  • changelogs/fragments/10983.yml (1 hunks)
  • changelogs/fragments/10999.yml (1 hunks)
  • src/core/public/chat/chat_service.mock.ts (1 hunks)
  • src/core/public/chat/chat_service.test.ts (1 hunks)
  • src/core/public/chat/chat_service.ts (1 hunks)
  • src/core/public/chat/index.ts (1 hunks)
  • src/core/public/chat/types.ts (1 hunks)
  • src/core/public/core_system.ts (9 hunks)
  • src/core/public/index.ts (4 hunks)
  • src/core/public/mocks.ts (3 hunks)
  • src/core/public/plugins/plugin_context.ts (2 hunks)
  • src/core/public/plugins/plugins_service.test.ts (3 hunks)
  • src/plugins/chat/common/chat_capabilities.test.ts (1 hunks)
  • src/plugins/chat/common/chat_capabilities.ts (1 hunks)
  • src/plugins/chat/common/types.ts (2 hunks)
  • src/plugins/chat/opensearch_dashboards.json (1 hunks)
  • src/plugins/chat/public/actions/graph_timeseries_data_action.test.tsx (1 hunks)
  • src/plugins/chat/public/actions/graph_timeseries_data_action.tsx (2 hunks)
  • src/plugins/chat/public/components/chat_header_button.test.tsx (14 hunks)
  • src/plugins/chat/public/components/chat_header_button.tsx (5 hunks)
  • src/plugins/chat/public/components/chat_window.tsx (5 hunks)
  • src/plugins/chat/public/components/graph_visualization/error_boundary.tsx (0 hunks)
  • src/plugins/chat/public/components/graph_visualization/error_display.tsx (0 hunks)
  • src/plugins/chat/public/components/graph_visualization/error_handler.ts (0 hunks)
  • src/plugins/chat/public/components/graph_visualization/validation.ts (0 hunks)
  • src/plugins/chat/public/plugin.test.ts (4 hunks)
  • src/plugins/chat/public/plugin.ts (6 hunks)
  • src/plugins/chat/public/services/ag_ui_agent.ts (2 hunks)
  • src/plugins/chat/public/services/chat_context_manager.test.ts (0 hunks)
  • src/plugins/chat/public/services/chat_context_manager.ts (0 hunks)
  • src/plugins/chat/public/services/chat_event_handler.test.ts (1 hunks)
  • src/plugins/chat/public/services/chat_event_handler.ts (1 hunks)
  • src/plugins/chat/public/services/chat_service.test.ts (16 hunks)
  • src/plugins/chat/public/services/chat_service.ts (15 hunks)
  • src/plugins/chat/public/types.ts (2 hunks)
  • src/plugins/chat/server/index.ts (1 hunks)
  • src/plugins/chat/server/plugin.ts (1 hunks)
  • src/plugins/chat/server/routes/index.test.ts (4 hunks)
  • src/plugins/chat/server/routes/index.ts (3 hunks)
  • src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts (1 hunks)
  • src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1 hunks)
  • src/plugins/chat/server/routes/ml_routes/ml_commons_agent.test.ts (0 hunks)
  • src/plugins/chat/server/routes/ml_routes/ml_commons_agent.ts (0 hunks)
  • src/plugins/chat/server/routes/ml_routes/router_registry.ts (1 hunks)
  • src/plugins/data_source_management/public/index.ts (1 hunks)
  • src/plugins/explore/opensearch_dashboards.json (1 hunks)
  • src/plugins/explore/public/actions/ask_ai_action.test.ts (3 hunks)
  • src/plugins/explore/public/actions/ask_ai_action.ts (1 hunks)
  • src/plugins/explore/public/build_services.ts (1 hunks)
  • src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.test.tsx (3 hunks)
  • src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.tsx (3 hunks)
  • src/plugins/explore/public/components/tabs/action_bar/action_bar.tsx (2 hunks)
  • src/plugins/explore/public/plugin.ts (1 hunks)
  • src/plugins/explore/public/types.ts (1 hunks)
💤 Files with no reviewable changes (8)
  • src/plugins/chat/server/routes/ml_routes/ml_commons_agent.ts
  • src/plugins/chat/public/components/graph_visualization/validation.ts
  • src/plugins/chat/server/routes/ml_routes/ml_commons_agent.test.ts
  • src/plugins/chat/public/components/graph_visualization/error_display.tsx
  • src/plugins/chat/public/services/chat_context_manager.ts
  • src/plugins/chat/public/services/chat_context_manager.test.ts
  • src/plugins/chat/public/components/graph_visualization/error_boundary.tsx
  • src/plugins/chat/public/components/graph_visualization/error_handler.ts
🧰 Additional context used
🧬 Code graph analysis (28)
src/plugins/chat/public/services/chat_event_handler.test.ts (1)
src/core/public/chat/types.ts (1)
  • ToolMessage (71-77)
src/plugins/explore/public/types.ts (1)
src/plugins/explore/public/services/slot_registry/slot_registry_service.ts (1)
  • SlotRegistryService (65-116)
src/plugins/chat/public/services/ag_ui_agent.ts (1)
src/plugins/chat/common/types.ts (1)
  • RunAgentInput (111-111)
src/plugins/chat/server/routes/ml_routes/router_registry.ts (4)
src/core/public/index.ts (1)
  • Capabilities (361-361)
src/plugins/chat/common/chat_capabilities.ts (1)
  • hasInvestigationCapabilities (26-28)
src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1)
  • MLAgentRouterFactory (49-74)
src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts (1)
  • GenericMLRouter (79-176)
src/plugins/explore/public/actions/ask_ai_action.test.ts (2)
src/core/public/chat/types.ts (1)
  • ChatServiceStart (195-203)
src/plugins/explore/public/actions/ask_ai_action.ts (1)
  • createAskAiAction (13-29)
src/plugins/chat/public/plugin.test.ts (2)
src/core/public/chat/chat_service.ts (1)
  • ChatService (19-169)
src/plugins/chat/public/plugin.ts (1)
  • ChatPlugin (41-193)
src/plugins/chat/public/types.ts (1)
src/plugins/data_source_management/public/index.ts (1)
  • DataSourceManagementPluginSetup (18-18)
src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1)
src/core/server/index.ts (4)
  • RequestHandlerContext (436-460)
  • OpenSearchDashboardsRequest (187-187)
  • OpenSearchDashboardsResponseFactory (215-215)
  • IOpenSearchDashboardsResponse (191-191)
src/core/public/index.ts (1)
src/core/public/chat/types.ts (2)
  • ChatServiceSetup (170-190)
  • ChatServiceStart (195-203)
src/core/public/plugins/plugins_service.test.ts (3)
src/core/public/chat/chat_service.mock.ts (1)
  • coreChatServiceMock (50-53)
src/core/public/chat/index.ts (1)
  • coreChatServiceMock (24-24)
src/core/public/mocks.ts (1)
  • coreChatServiceMock (68-68)
src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts (2)
src/core/server/index.ts (4)
  • RequestHandlerContext (436-460)
  • OpenSearchDashboardsRequest (187-187)
  • OpenSearchDashboardsResponseFactory (215-215)
  • IOpenSearchDashboardsResponse (191-191)
src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1)
  • MLAgentRouter (19-43)
src/core/public/chat/chat_service.test.ts (2)
src/core/public/chat/chat_service.ts (1)
  • ChatService (19-169)
src/core/public/chat/types.ts (1)
  • ChatImplementationFunctions (149-165)
src/core/public/chat/chat_service.mock.ts (4)
src/core/public/chat/types.ts (2)
  • ChatServiceSetup (170-190)
  • ChatServiceStart (195-203)
src/core/public/chat/index.ts (3)
  • ChatServiceSetup (8-8)
  • ChatServiceStart (9-9)
  • coreChatServiceMock (24-24)
src/core/public/index.ts (2)
  • ChatServiceSetup (439-439)
  • ChatServiceStart (440-440)
src/core/public/mocks.ts (1)
  • coreChatServiceMock (68-68)
src/plugins/chat/public/plugin.ts (3)
src/core/public/index.ts (2)
  • CoreSetup (262-298)
  • CoreStart (321-358)
src/core/public/chat/chat_service.ts (1)
  • ChatService (19-169)
src/core/public/chat/index.ts (1)
  • ChatService (23-23)
src/plugins/explore/public/components/tabs/action_bar/action_bar.tsx (1)
src/plugins/opensearch_dashboards_utils/public/index.ts (1)
  • of (41-41)
src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.tsx (4)
src/plugins/explore/public/types/log_actions.ts (1)
  • LogActionItemProps (37-46)
src/core/public/chat/types.ts (1)
  • ChatServiceStart (195-203)
src/core/public/index.ts (1)
  • ChatServiceStart (440-440)
src/plugins/context_provider/public/hooks/use_dynamic_context.ts (1)
  • useDynamicContext (18-78)
src/plugins/chat/server/routes/index.test.ts (1)
src/plugins/chat/server/routes/index.ts (1)
  • defineRoutes (81-161)
src/core/public/chat/chat_service.ts (2)
src/core/public/chat/types.ts (5)
  • ChatServiceSetup (170-190)
  • ChatServiceStart (195-203)
  • ChatImplementationFunctions (149-165)
  • ChatWindowState (97-101)
  • Message (82-87)
src/core/public/index.ts (5)
  • ChatServiceSetup (439-439)
  • ChatServiceStart (440-440)
  • ChatImplementationFunctions (441-441)
  • ChatWindowState (443-443)
  • Message (442-442)
src/plugins/chat/common/chat_capabilities.ts (1)
src/plugins/chat/server/index.ts (1)
  • config (13-19)
src/plugins/chat/public/actions/graph_timeseries_data_action.test.tsx (1)
src/plugins/chat/public/actions/graph_timeseries_data_action.tsx (1)
  • useGraphTimeseriesDataAction (42-250)
src/plugins/chat/common/chat_capabilities.test.ts (2)
src/plugins/chat/common/chat_capabilities.ts (4)
  • hasInvestigationCapabilities (26-28)
  • ContextProviderConfig (17-19)
  • ChatConfig (9-12)
  • isChatEnabled (42-59)
src/plugins/chat/server/index.ts (1)
  • config (13-19)
src/plugins/chat/public/components/chat_header_button.test.tsx (2)
src/core/public/mocks.ts (1)
  • coreMock (206-213)
src/plugins/chat/public/components/chat_header_button.tsx (1)
  • ChatHeaderButton (39-251)
src/plugins/explore/public/build_services.ts (2)
src/plugins/explore/public/services/slot_registry/index.ts (1)
  • SlotRegistryService (7-7)
src/plugins/explore/public/services/slot_registry/slot_registry_service.ts (1)
  • SlotRegistryService (65-116)
src/plugins/explore/public/plugin.ts (2)
src/plugins/explore/public/actions/ask_ai_action.ts (1)
  • createAskAiAction (13-29)
src/plugins/explore/public/services/log_action_registry.ts (1)
  • logActionRegistry (76-76)
src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.test.tsx (3)
src/core/public/chat/types.ts (1)
  • ChatServiceStart (195-203)
src/core/public/chat/index.ts (1)
  • ChatServiceStart (9-9)
src/core/public/index.ts (1)
  • ChatServiceStart (440-440)
src/plugins/chat/public/components/chat_window.tsx (1)
src/core/public/chat/types.ts (1)
  • Message (82-87)
src/plugins/chat/server/routes/index.ts (2)
src/plugins/chat/server/routes/ml_routes/router_registry.ts (1)
  • MLAgentRouterRegistry (16-50)
src/plugins/chat/server/routes/ml_routes/ml_agent_router.ts (1)
  • MLAgentRouterFactory (49-74)
src/plugins/chat/public/services/chat_service.test.ts (4)
src/core/public/chat/chat_service.ts (1)
  • ChatService (19-169)
src/plugins/chat/public/services/ag_ui_agent.ts (1)
  • AgUiAgent (22-142)
src/core/public/chat/types.ts (1)
  • ChatServiceStart (195-203)
src/core/public/index.ts (1)
  • ChatServiceStart (440-440)
🪛 YAMLlint (1.37.1)
changelogs/fragments/10983.yml

[error] 2-2: syntax error: expected , but found ''

(syntax)

changelogs/fragments/10999.yml

[error] 2-2: syntax error: expected , but found '['

(syntax)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (65)
  • GitHub Check: Run cypress tests (osd:ciGroup17Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup14Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup13Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup15Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup16Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup15)
  • GitHub Check: Run cypress tests (osd:ciGroup13)
  • GitHub Check: Run cypress tests (osd:ciGroup12Explore)
  • GitHub Check: Run cypress tests (osd:ciGroup14)
  • GitHub Check: Run cypress tests (osd:ciGroup12)
  • GitHub Check: Run cypress tests (osd:ciGroup10Slow)
  • GitHub Check: Run cypress tests (osd:ciGroup7)
  • GitHub Check: Run cypress tests (osd:ciGroup11)
  • GitHub Check: Run cypress tests (osd:ciGroup9)
  • GitHub Check: Run cypress tests (osd:ciGroup5)
  • GitHub Check: Run cypress tests (osd:ciGroup8)
  • GitHub Check: Run cypress tests (osd:ciGroup10Fast)
  • GitHub Check: Run cypress tests (osd:ciGroup3)
  • GitHub Check: Run cypress tests (osd:ciGroup6)
  • GitHub Check: Run cypress tests (osd:ciGroup1)
  • GitHub Check: Run cypress tests (osd:ciGroup4)
  • GitHub Check: Run cypress tests (osd:ciGroup2)
  • GitHub Check: bundle-analyzer
  • GitHub Check: Run functional tests on Windows (ciGroup12)
  • GitHub Check: Run functional tests on Windows (ciGroup5)
  • GitHub Check: Run functional tests on Linux (ciGroup1)
  • GitHub Check: Run functional tests on Linux (ciGroup3)
  • GitHub Check: Run functional tests on Linux (ciGroup11)
  • GitHub Check: lighthouse
  • GitHub Check: Run functional tests on Linux (ciGroup10)
  • GitHub Check: Run functional tests on Windows (ciGroup1)
  • GitHub Check: Run functional tests on Linux (ciGroup12)
  • GitHub Check: Run functional tests on Linux (ciGroup7)
  • GitHub Check: Run functional tests on Windows (ciGroup8)
  • GitHub Check: Run functional tests on Windows (ciGroup2)
  • GitHub Check: Run functional tests on Windows (ciGroup11)
  • GitHub Check: Build min release artifacts on Linux ARM64
  • GitHub Check: Run functional tests on Windows (ciGroup3)
  • GitHub Check: Run functional tests on Linux (ciGroup9)
  • GitHub Check: Run functional tests on Windows (ciGroup13)
  • GitHub Check: Run functional tests on Linux (ciGroup13)
  • GitHub Check: Run functional tests on Windows (ciGroup6)
  • GitHub Check: Run functional tests on Linux (ciGroup6)
  • GitHub Check: Run functional tests on Windows (ciGroup7)
  • GitHub Check: Run functional tests on Linux (ciGroup5)
  • GitHub Check: Run functional tests on Windows (ciGroup9)
  • GitHub Check: Run functional tests on Windows (ciGroup10)
  • GitHub Check: Run functional tests on Windows (ciGroup4)
  • GitHub Check: Run functional tests on Linux (ciGroup4)
  • GitHub Check: Run functional tests on Linux (ciGroup2)
  • GitHub Check: Build and Verify on Windows (ciGroup4)
  • GitHub Check: Run functional tests on Linux (ciGroup8)
  • GitHub Check: Build min release artifacts on Linux x64
  • GitHub Check: Build min release artifacts on Windows x64
  • GitHub Check: Build min release artifacts on macOS ARM64
  • GitHub Check: Build min release artifacts on macOS x64
  • GitHub Check: Run plugin functional tests on Windows
  • GitHub Check: Run plugin functional tests on Linux
  • GitHub Check: Build and Verify on Windows (ciGroup1)
  • GitHub Check: Build and Verify on Windows (ciGroup2)
  • GitHub Check: Build and Verify on Linux (ciGroup1)
  • GitHub Check: Build and Verify on Windows (ciGroup3)
  • GitHub Check: Build and Verify on Linux (ciGroup4)
  • GitHub Check: Build and Verify on Linux (ciGroup2)
  • GitHub Check: Build and Verify on Linux (ciGroup3)

Comment on lines +1 to +2
refactor:
- [Chat] Refactor chat architecture: Extract shared infrastructure into a core chat service ([#10983](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10983)) No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote the list item to fix YAML syntax error

Same issue here: the list value starts with [, which is not allowed for an unquoted scalar.

refactor:
-- [Chat] Refactor chat architecture: Extract shared infrastructure into a core chat service ([#10983](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10983))
+- "[Chat] Refactor chat architecture: Extract shared infrastructure into a core chat service ([#10983](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10983))"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
refactor:
- [Chat] Refactor chat architecture: Extract shared infrastructure into a core chat service ([#10983](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10983))
refactor:
- "[Chat] Refactor chat architecture: Extract shared infrastructure into a core chat service ([#10983](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10983))"
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 2-2: syntax error: expected , but found ''

(syntax)

🤖 Prompt for AI Agents
In changelogs/fragments/10983.yml around lines 1 to 2, the list item begins with
'[' which breaks YAML parsing; wrap the entire list string in quotes (e.g. '"'
or "'") so the scalar is quoted, preserving the content exactly (including the
bracketed PR link) to fix the YAML syntax error.

Comment on lines +1 to +2
fix:
- [BUG][Chat] Unregister tools when component unmount ([#10999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10999)) No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote the list item to fix YAML syntax error

YAML plain scalars cannot start with [; this fragment will fail parsing. Wrap the string in quotes.

fix:
-- [BUG][Chat] Unregister tools when component unmount ([#10999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10999))
+- "[BUG][Chat] Unregister tools when component unmount ([#10999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10999))"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fix:
- [BUG][Chat] Unregister tools when component unmount ([#10999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10999))
fix:
- "[BUG][Chat] Unregister tools when component unmount ([#10999](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10999))"
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 2-2: syntax error: expected , but found '['

(syntax)

🤖 Prompt for AI Agents
In changelogs/fragments/10999.yml around lines 1 to 2, the YAML fragment uses a
plain scalar that starts with '[' which is invalid; wrap the entire list item
string in quotes (single or double) so the line becomes a quoted string value
under fix:, preserving the same text content and indentation to ensure the
fragment parses as valid YAML.

Comment on lines +20 to +21
let cachedClientKey: string | null = null;
let detectionAttempted = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Module-level mutable state causes cross-request contamination.

cachedClientKey and detectionAttempted are module-level variables shared across all incoming requests. In a Node.js server, this module is loaded once, so:

  1. If the first request has a certain context structure, all subsequent requests assume the same structure
  2. In multi-cluster or multi-tenant scenarios, different requests may have different context providers
  3. The cache is never invalidated, causing stale results if ML client availability changes

Move the caching to be request-scoped or remove it entirely:

-let cachedClientKey: string | null = null;
-let detectionAttempted = false;
-
-function findMLClient(context: RequestHandlerContext) {
-  // Use cached result if available
-  if (detectionAttempted) {
-    if (cachedClientKey) {
-      const contextItem = (context as any)[cachedClientKey];
-      return contextItem?.client;
-    }
-    return undefined;
-  }
-
-  // First-time detection - cache the result
-  for (const key of Object.keys(context)) {
-    const contextItem = (context as any)[key];
-    if (contextItem?.client?.request && typeof contextItem.client.request === 'function') {
-      cachedClientKey = key; // Cache the key name
-      detectionAttempted = true;
-      return contextItem.client;
-    }
-  }
-
-  detectionAttempted = true; // Mark as attempted even if not found
-  return undefined;
-}
+function findMLClient(context: RequestHandlerContext) {
+  for (const key of Object.keys(context)) {
+    const contextItem = (context as any)[key];
+    if (contextItem?.client?.request && typeof contextItem.client.request === 'function') {
+      return contextItem.client;
+    }
+  }
+  return undefined;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let cachedClientKey: string | null = null;
let detectionAttempted = false;
function findMLClient(context: RequestHandlerContext) {
for (const key of Object.keys(context)) {
const contextItem = (context as any)[key];
if (contextItem?.client?.request && typeof contextItem.client.request === 'function') {
return contextItem.client;
}
}
return undefined;
}
🤖 Prompt for AI Agents
In src/plugins/chat/server/routes/ml_routes/generic_ml_router.ts around lines
20-21, the module-level variables `cachedClientKey` and `detectionAttempted`
create cross-request state; move the cache to request scope or remove it: stop
using module-level mutable variables and instead compute/detect the ML client
per request (or attach a cache to the request object or per-tenant context) and
ensure any cache has proper invalidation/TTL tied to the request/tenant
lifecycle; update all call sites to read/write the per-request/tenant cache and
remove the global variables so no stale or shared state persists across requests
or clusters.

Comment on lines +67 to +68
// Register context with cleanup on unmount (true = cleanup when component unmounts)
useDynamicContext(contextData, false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Comment contradicts the actual behavior—context will NOT be cleaned up on unmount.

The comment states "cleanup on unmount" but false is passed as the second parameter. According to useDynamicContext signature (from src/plugins/context_provider/public/hooks/use_dynamic_context.ts), shouldCleanupOnUnmount = true enables cleanup on unmount. Passing false explicitly disables cleanup.

This contradicts the PR objective to "Unregister tools when component unmount". Either:

  1. Change false to true to enable cleanup, or
  2. Update the comment to reflect the intentional no-cleanup behavior

If cleanup is desired per the PR objective:

-  // Register context with cleanup on unmount (true = cleanup when component unmounts)
-  useDynamicContext(contextData, false);
+  // Register context with cleanup on unmount
+  useDynamicContext(contextData, true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register context with cleanup on unmount (true = cleanup when component unmounts)
useDynamicContext(contextData, false);
// Register context with cleanup on unmount
useDynamicContext(contextData, true);
🤖 Prompt for AI Agents
In
src/plugins/explore/public/components/ask_ai_action_item/ask_ai_action_item.tsx
around lines 67-68 the comment says "cleanup on unmount" but the call uses
useDynamicContext(contextData, false) which disables cleanup; to satisfy the PR
objective of unregistering tools on unmount, change the second argument from
false to true so the hook will perform cleanup on component unmount (or if the
no-cleanup behavior was intentional, update the comment to reflect that cleanup
is disabled).

@codecov
Copy link
Copy Markdown

codecov bot commented Dec 1, 2025

Codecov Report

❌ Patch coverage is 57.14286% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.77%. Comparing base (d8b1c9c) to head (9f775aa).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/plugins/chat/public/services/chat_service.ts 50.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10999      +/-   ##
==========================================
- Coverage   60.77%   60.77%   -0.01%     
==========================================
  Files        4537     4537              
  Lines      122381   122388       +7     
  Branches    20535    20536       +1     
==========================================
- Hits        74381    74380       -1     
- Misses      42743    42751       +8     
  Partials     5257     5257              
Flag Coverage Δ
Linux_1 26.55% <ø> (ø)
Linux_2 38.97% <ø> (ø)
Linux_3 39.43% <57.14%> (-0.01%) ⬇️
Linux_4 33.73% <ø> (ø)
Windows_1 26.56% <ø> (ø)
Windows_2 38.95% <ø> (ø)
Windows_3 39.44% <57.14%> (-0.01%) ⬇️
Windows_4 33.73% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: Anan Zhuang <ananzh@amazon.com>
@ananzh ananzh force-pushed the chat-fix-unregister-tool branch from b301383 to 9ffe6e9 Compare December 3, 2025 05:33
@ananzh ananzh added ai Changes related to AI chat, context provider and artifact bug Something isn't working v3.4.0 labels Dec 3, 2025
@ananzh ananzh merged commit f3289a5 into opensearch-project:main Dec 3, 2025
109 of 114 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 9, 2025
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Changes related to AI chat, context provider and artifact bug Something isn't working distinguished-contributor v3.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants