Skip to content

Commit adfd970

Browse files
committed
OLS-2722: olsToolUIs initial support
Allows plugins to define 'ols.tool-ui' extensions to map the tools to react components to be rendered when the tool gets called. Sample extensions registration: ```json { "type": "ols.tool-ui", "properties": { "id": "my-obs/my-tool", "component": { "$codeRef": "MyToolUI" } } } ``` The ToolUI implemntation receives the tool details in it's argument: ```ts type MyTool = { name: 'my-tool'; args: object, // ... }; export const MyToolUI React.FC<{ tool: MyTool }> = ({ tool }) => { // component implementation } ```
1 parent 631a46c commit adfd970

5 files changed

Lines changed: 105 additions & 9 deletions

File tree

src/components/OlsToolUIs.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from 'react';
2+
import { Map as ImmutableMap } from 'immutable';
3+
import { useSelector } from 'react-redux';
4+
import { State } from '../redux-reducers';
5+
import { useToolUIMapping } from '../hooks/useToolUIMapping';
6+
import type { OlsToolUIComponent, Tool } from '../types';
7+
8+
type OlsToolUIProps = {
9+
tool: Tool;
10+
toolUIComponent: OlsToolUIComponent;
11+
};
12+
13+
export const OlsToolUI: React.FC<OlsToolUIProps> = ({ tool, toolUIComponent: toolUIElement }) => {
14+
const ToolComponent = toolUIElement;
15+
return <ToolComponent tool={tool} />;
16+
};
17+
18+
type OlsUIToolsProps = {
19+
entryIndex: number;
20+
};
21+
22+
export const OlsToolUIs: React.FC<OlsUIToolsProps> = ({ entryIndex }) => {
23+
const [toolUIMapping] = useToolUIMapping();
24+
25+
const toolsData: ImmutableMap<string, ImmutableMap<string, unknown>> = useSelector((s: State) =>
26+
s.plugins?.ols?.getIn(['chatHistory', entryIndex, 'tools']),
27+
);
28+
29+
const olsToolsWithUI = toolsData
30+
.map((value) => {
31+
const tool = value.toJS() as Tool;
32+
const olsUiID = tool.tool_meta?.olsUi?.id;
33+
const toolUIComponent = olsUiID && toolUIMapping[olsUiID];
34+
return { tool, toolUIComponent };
35+
})
36+
.filter(({ toolUIComponent }) => !!toolUIComponent);
37+
38+
return (
39+
<>
40+
{olsToolsWithUI
41+
.map(({ tool, toolUIComponent }, toolID) => (
42+
<OlsToolUI key={`ols-app-${toolID}`} tool={tool} toolUIComponent={toolUIComponent} />
43+
))
44+
.valueSeq()}
45+
</>
46+
);
47+
};
48+
49+
export default OlsToolUIs;

src/components/Prompt.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,8 @@ const Prompt: React.FC<PromptProps> = ({ scrollIntoView }) => {
581581
const { args, id, name: toolName } = json.data;
582582
dispatch(chatHistoryUpdateTool(chatEntryID, id, { name: toolName, args }));
583583
} else if (json.event === 'tool_result') {
584-
const { content, id, status } = json.data;
585-
dispatch(chatHistoryUpdateTool(chatEntryID, id, { content, status }));
584+
const { content, id, status, tool_meta } = json.data;
585+
dispatch(chatHistoryUpdateTool(chatEntryID, id, { content, status, tool_meta }));
586586
} else if (json.event === 'error') {
587587
dispatchTokens.flush();
588588
dispatch(

src/components/ResponseTools.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CodeIcon, InfoCircleIcon } from '@patternfly/react-icons';
66

77
import { openToolSet } from '../redux-actions';
88
import { State } from '../redux-reducers';
9+
import OlsToolUIs from './OlsToolUIs';
910

1011
type ToolProps = {
1112
entryIndex: number;
@@ -47,11 +48,14 @@ const ResponseTools: React.FC<ResponseToolsProps> = ({ entryIndex }) => {
4748
);
4849

4950
return (
50-
<LabelGroup numLabels={4}>
51-
{tools.keySeq().map((toolID) => (
52-
<ToolLabel entryIndex={entryIndex} key={toolID} toolID={toolID} />
53-
))}
54-
</LabelGroup>
51+
<>
52+
<LabelGroup numLabels={4}>
53+
{tools.keySeq().map((toolID) => (
54+
<ToolLabel entryIndex={entryIndex} key={toolID} toolID={toolID} />
55+
))}
56+
</LabelGroup>
57+
<OlsToolUIs entryIndex={entryIndex} />
58+
</>
5559
);
5660
};
5761

src/hooks/useToolUIMapping.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as React from 'react';
2+
import { useResolvedExtensions } from '@openshift-console/dynamic-plugin-sdk';
3+
import type {
4+
CodeRef,
5+
Extension,
6+
ExtensionDeclaration,
7+
} from '@openshift-console/dynamic-plugin-sdk/lib/types';
8+
import type { OlsToolUIComponent } from '../types';
9+
10+
type ToolUIExtensionProperties = {
11+
/** Id of the component (as refferrenced by the mcp tool) */
12+
id: string;
13+
/** The component to be rendered when the MCP tool matches. */
14+
component: CodeRef<OlsToolUIComponent>;
15+
};
16+
17+
type ToolUIExtension = ExtensionDeclaration<'ols.tool-ui', ToolUIExtensionProperties>;
18+
19+
const isToolUIExtension = (e: Extension): e is ToolUIExtension => e.type === 'ols.tool-ui';
20+
21+
export const useToolUIExtensions = () => useResolvedExtensions(isToolUIExtension);
22+
23+
export const useToolUIMapping = (): [Record<string, OlsToolUIComponent>, boolean] => {
24+
const [extensions, resolved] = useToolUIExtensions();
25+
26+
const mapping = React.useMemo(() => {
27+
const result: Record<string, OlsToolUIComponent> = {};
28+
extensions.forEach((extension) => {
29+
const { id, component } = extension.properties as {
30+
id: string;
31+
component: OlsToolUIComponent;
32+
};
33+
result[id] = component;
34+
});
35+
return result;
36+
}, [extensions]);
37+
38+
return [mapping, resolved];
39+
};

src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from 'react';
12
import { Map as ImmutableMap } from 'immutable';
23

34
import { ErrorType } from './error';
@@ -27,12 +28,15 @@ export type ReferencedDoc = {
2728
};
2829

2930
export type Tool = {
30-
args: { [key: string]: Array<string> };
31+
args: { [key: string]: string };
3132
content: string;
3233
name: string;
33-
status: 'error' | 'success';
34+
status: 'error' | 'success' | 'truncated';
35+
tool_meta?: { olsUi?: { id: string } };
3436
};
3537

38+
export type OlsToolUIComponent = React.ComponentType<{ tool: Tool }>;
39+
3640
type ChatEntryUser = {
3741
attachments: { [key: string]: Attachment };
3842
text: string;

0 commit comments

Comments
 (0)