feat(mcp): elicitation support (#5965)
This commit is contained in:
@@ -2428,6 +2428,50 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"message",
|
||||
"requested_schema",
|
||||
"actionType"
|
||||
],
|
||||
"properties": {
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"elicitation"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"requested_schema": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"user_data",
|
||||
"actionType"
|
||||
],
|
||||
"properties": {
|
||||
"actionType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"elicitationResponse"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_data": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
|
||||
@@ -14,6 +14,15 @@ export type ActionRequiredData = {
|
||||
id: string;
|
||||
prompt?: string | null;
|
||||
toolName: string;
|
||||
} | {
|
||||
actionType: 'elicitation';
|
||||
id: string;
|
||||
message: string;
|
||||
requested_schema: unknown;
|
||||
} | {
|
||||
actionType: 'elicitationResponse';
|
||||
id: string;
|
||||
user_data: unknown;
|
||||
};
|
||||
|
||||
export type AddExtensionRequest = {
|
||||
|
||||
@@ -96,6 +96,7 @@ function BaseChatContent({
|
||||
messages,
|
||||
chatState,
|
||||
handleSubmit,
|
||||
submitElicitationResponse,
|
||||
stopStreaming,
|
||||
sessionLoadError,
|
||||
setRecipeUserParams,
|
||||
@@ -274,6 +275,7 @@ function BaseChatContent({
|
||||
isStreamingMessage={chatState !== ChatState.Idle}
|
||||
onRenderingComplete={handleRenderingComplete}
|
||||
onMessageUpdate={onMessageUpdate}
|
||||
submitElicitationResponse={submitElicitationResponse}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useState } from 'react';
|
||||
import { ActionRequired } from '../api';
|
||||
import JsonSchemaForm from './ui/JsonSchemaForm';
|
||||
import type { JsonSchema } from './ui/JsonSchemaForm';
|
||||
|
||||
interface ElicitationRequestProps {
|
||||
isCancelledMessage: boolean;
|
||||
isClicked: boolean;
|
||||
actionRequiredContent: ActionRequired & { type: 'actionRequired' };
|
||||
onSubmit: (elicitationId: string, userData: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
export default function ElicitationRequest({
|
||||
isCancelledMessage,
|
||||
isClicked,
|
||||
actionRequiredContent,
|
||||
onSubmit,
|
||||
}: ElicitationRequestProps) {
|
||||
const [submitted, setSubmitted] = useState(isClicked);
|
||||
|
||||
if (actionRequiredContent.data.actionType !== 'elicitation') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id: elicitationId, message, requested_schema } = actionRequiredContent.data;
|
||||
|
||||
const handleSubmit = (formData: Record<string, unknown>) => {
|
||||
setSubmitted(true);
|
||||
onSubmit(elicitationId, formData);
|
||||
};
|
||||
|
||||
if (isCancelledMessage) {
|
||||
return (
|
||||
<div className="goose-message-content bg-background-muted rounded-2xl px-4 py-2 text-textStandard">
|
||||
Information request was cancelled.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="goose-message-content bg-background-muted rounded-2xl px-4 py-2 text-textStandard">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>Information submitted</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="goose-message-content bg-background-muted rounded-2xl rounded-b-none px-4 py-2 text-textStandard">
|
||||
{message || 'Goose needs some information from you.'}
|
||||
</div>
|
||||
<div className="goose-message-content bg-background-default border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 py-3">
|
||||
<JsonSchemaForm
|
||||
schema={requested_schema as JsonSchema}
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
getToolRequests,
|
||||
getToolResponses,
|
||||
getToolConfirmationContent,
|
||||
getElicitationContent,
|
||||
NotificationEvent,
|
||||
} from '../types/message';
|
||||
import { Message, confirmToolAction } from '../api';
|
||||
import ToolCallConfirmation from './ToolCallConfirmation';
|
||||
import ElicitationRequest from './ElicitationRequest';
|
||||
import MessageCopyLink from './MessageCopyLink';
|
||||
import { cn } from '../utils';
|
||||
import { identifyConsecutiveToolCalls, shouldHideTimestamp } from '../utils/toolCallChaining';
|
||||
@@ -28,6 +30,10 @@ interface GooseMessageProps {
|
||||
toolCallNotifications: Map<string, NotificationEvent[]>;
|
||||
append: (value: string) => void;
|
||||
isStreaming?: boolean; // Whether this message is currently being streamed
|
||||
submitElicitationResponse?: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function GooseMessage({
|
||||
@@ -38,6 +44,7 @@ export default function GooseMessage({
|
||||
toolCallNotifications,
|
||||
append,
|
||||
isStreaming = false,
|
||||
submitElicitationResponse,
|
||||
}: GooseMessageProps) {
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const handledToolConfirmations = useRef<Set<string>>(new Set());
|
||||
@@ -69,12 +76,14 @@ export default function GooseMessage({
|
||||
const toolRequests = getToolRequests(message);
|
||||
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
||||
const toolConfirmationContent = getToolConfirmationContent(message);
|
||||
const elicitationContent = getElicitationContent(message);
|
||||
const toolCallChains = useMemo(() => identifyConsecutiveToolCalls(messages), [messages]);
|
||||
const hideTimestamp = useMemo(
|
||||
() => shouldHideTimestamp(messageIndex, toolCallChains),
|
||||
[messageIndex, toolCallChains]
|
||||
);
|
||||
const hasToolConfirmation = toolConfirmationContent !== undefined;
|
||||
const hasElicitation = elicitationContent !== undefined;
|
||||
|
||||
const toolResponsesMap = useMemo(() => {
|
||||
const responseMap = new Map();
|
||||
@@ -219,6 +228,15 @@ export default function GooseMessage({
|
||||
actionRequiredContent={toolConfirmationContent}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasElicitation && submitElicitationResponse && (
|
||||
<ElicitationRequest
|
||||
isCancelledMessage={messageIndex == messageHistoryIndex - 1}
|
||||
isClicked={messageIndex < messageHistoryIndex}
|
||||
actionRequiredContent={elicitationContent}
|
||||
onSubmit={submitElicitationResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,10 @@ interface ProgressiveMessageListProps {
|
||||
isStreamingMessage?: boolean; // Whether messages are currently being streamed
|
||||
onMessageUpdate?: (messageId: string, newContent: string) => void;
|
||||
onRenderingComplete?: () => void; // Callback when all messages are rendered
|
||||
submitElicitationResponse?: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ProgressiveMessageList({
|
||||
@@ -53,6 +57,7 @@ export default function ProgressiveMessageList({
|
||||
isStreamingMessage = false, // Whether messages are currently being streamed
|
||||
onMessageUpdate,
|
||||
onRenderingComplete,
|
||||
submitElicitationResponse,
|
||||
}: ProgressiveMessageListProps) {
|
||||
const [renderedCount, setRenderedCount] = useState(() => {
|
||||
// Initialize with either all messages (if small) or first batch (if large)
|
||||
@@ -223,6 +228,7 @@ export default function ProgressiveMessageList({
|
||||
index === messagesToRender.length - 1 &&
|
||||
message.role === 'assistant'
|
||||
}
|
||||
submitElicitationResponse={submitElicitationResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -240,6 +246,7 @@ export default function ProgressiveMessageList({
|
||||
isStreamingMessage,
|
||||
onMessageUpdate,
|
||||
toolCallChains,
|
||||
submitElicitationResponse,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,8 @@ const toolConfirmationState = new Map<
|
||||
}
|
||||
>();
|
||||
|
||||
type ToolConfirmationData = Extract<ActionRequired['data'], { actionType: 'toolConfirmation' }>;
|
||||
|
||||
interface ToolConfirmationProps {
|
||||
sessionId: string;
|
||||
isCancelledMessage: boolean;
|
||||
@@ -33,7 +35,8 @@ export default function ToolConfirmation({
|
||||
isClicked,
|
||||
actionRequiredContent,
|
||||
}: ToolConfirmationProps) {
|
||||
const { id: toolConfirmationId, toolName, prompt } = actionRequiredContent.data;
|
||||
const data = actionRequiredContent.data as ToolConfirmationData;
|
||||
const { id: toolConfirmationId, toolName, prompt } = data;
|
||||
|
||||
// Check if we have a stored state for this tool confirmation
|
||||
const storedState = toolConfirmationState.get(toolConfirmationId);
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Input } from './input';
|
||||
import { Button } from './button';
|
||||
|
||||
interface JsonSchemaProperty {
|
||||
type?: string;
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
enum?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export interface JsonSchema {
|
||||
type?: string;
|
||||
properties?: Record<string, JsonSchemaProperty>;
|
||||
required?: string[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface JsonSchemaFormProps {
|
||||
schema: JsonSchema;
|
||||
onSubmit: (data: Record<string, unknown>) => void;
|
||||
onCancel?: () => void;
|
||||
submitLabel?: string;
|
||||
cancelLabel?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function JsonSchemaForm({
|
||||
schema,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitLabel = 'Submit',
|
||||
cancelLabel = 'Cancel',
|
||||
disabled = false,
|
||||
}: JsonSchemaFormProps) {
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>(() => {
|
||||
const initial: Record<string, unknown> = {};
|
||||
if (schema.properties) {
|
||||
for (const [key, prop] of Object.entries(schema.properties)) {
|
||||
if (prop.default !== undefined) {
|
||||
initial[key] = prop.default;
|
||||
} else if (prop.type === 'boolean') {
|
||||
initial[key] = false;
|
||||
} else if (prop.type === 'number' || prop.type === 'integer') {
|
||||
initial[key] = prop.minimum ?? 0;
|
||||
} else {
|
||||
initial[key] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return initial;
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateField = useCallback(
|
||||
(key: string, value: unknown): string | null => {
|
||||
const prop = schema.properties?.[key];
|
||||
if (!prop) return null;
|
||||
|
||||
const isRequired = schema.required?.includes(key);
|
||||
|
||||
if (isRequired && (value === '' || value === null || value === undefined)) {
|
||||
return 'This field is required';
|
||||
}
|
||||
|
||||
if (prop.type === 'string' && typeof value === 'string') {
|
||||
if (!isRequired && value === '') return null;
|
||||
|
||||
if (prop.minLength !== undefined && value.length < prop.minLength) {
|
||||
return `Minimum length is ${prop.minLength}`;
|
||||
}
|
||||
if (prop.maxLength !== undefined && value.length > prop.maxLength) {
|
||||
return `Maximum length is ${prop.maxLength}`;
|
||||
}
|
||||
}
|
||||
|
||||
if ((prop.type === 'number' || prop.type === 'integer') && typeof value === 'number') {
|
||||
if (prop.minimum !== undefined && value < prop.minimum) {
|
||||
return `Minimum value is ${prop.minimum}`;
|
||||
}
|
||||
if (prop.maximum !== undefined && value > prop.maximum) {
|
||||
return `Maximum value is ${prop.maximum}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[schema]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(key: string, value: unknown) => {
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const error = validateField(key, value);
|
||||
setErrors((prev) => {
|
||||
if (error) {
|
||||
return { ...prev, [key]: error };
|
||||
}
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[key];
|
||||
return newErrors;
|
||||
});
|
||||
},
|
||||
[validateField]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (schema.properties) {
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
const error = validateField(key, formData[key]);
|
||||
if (error) {
|
||||
newErrors[key] = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(formData);
|
||||
},
|
||||
[formData, onSubmit, schema.properties, validateField]
|
||||
);
|
||||
|
||||
const renderField = (key: string, prop: JsonSchemaProperty) => {
|
||||
const value = formData[key];
|
||||
const error = errors[key];
|
||||
const isRequired = schema.required?.includes(key);
|
||||
|
||||
if (prop.enum) {
|
||||
return (
|
||||
<select
|
||||
id={key}
|
||||
value={String(value ?? '')}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
disabled={disabled}
|
||||
className="flex h-9 w-full rounded-md border focus:border-border-strong hover:border-border-strong bg-background-default px-3 py-1 text-base transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
>
|
||||
{!isRequired && <option value="">Select...</option>}
|
||||
{prop.enum.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
if (prop.type === 'boolean') {
|
||||
return (
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={key}
|
||||
checked={Boolean(value)}
|
||||
onChange={(e) => handleChange(key, e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-textStandard">{prop.description || key}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (prop.type === 'number' || prop.type === 'integer') {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
id={key}
|
||||
value={String(value ?? '')}
|
||||
onChange={(e) => {
|
||||
const numValue =
|
||||
prop.type === 'integer' ? parseInt(e.target.value, 10) : parseFloat(e.target.value);
|
||||
handleChange(key, isNaN(numValue) ? '' : numValue);
|
||||
}}
|
||||
min={prop.minimum}
|
||||
max={prop.maximum}
|
||||
step={prop.type === 'integer' ? 1 : 'any'}
|
||||
disabled={disabled}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
id={key}
|
||||
value={String(value ?? '')}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
minLength={prop.minLength}
|
||||
maxLength={prop.maxLength}
|
||||
disabled={disabled}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
||||
return <div className="text-textSubtle text-sm">No fields to display</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{Object.entries(schema.properties).map(([key, prop]) => {
|
||||
const isRequired = schema.required?.includes(key);
|
||||
const error = errors[key];
|
||||
|
||||
if (prop.type === 'boolean') {
|
||||
return (
|
||||
<div key={key} className="flex flex-col gap-1">
|
||||
{renderField(key, prop)}
|
||||
{error && <span className="text-red-500 text-xs">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} className="flex flex-col gap-1">
|
||||
<label htmlFor={key} className="text-sm font-medium text-textStandard">
|
||||
{key}
|
||||
{isRequired && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
{prop.description && prop.type !== 'boolean' && (
|
||||
<span className="text-xs text-textSubtle">{prop.description}</span>
|
||||
)}
|
||||
{renderField(key, prop)}
|
||||
{error && <span className="text-red-500 text-xs">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button type="submit" disabled={disabled}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={disabled}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
import {
|
||||
createUserMessage,
|
||||
createElicitationResponseMessage,
|
||||
getCompactingMessage,
|
||||
getThinkingMessage,
|
||||
NotificationEvent,
|
||||
@@ -33,6 +34,10 @@ interface UseChatStreamReturn {
|
||||
messages: Message[];
|
||||
chatState: ChatState;
|
||||
handleSubmit: (userMessage: string) => Promise<void>;
|
||||
submitElicitationResponse: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
setRecipeUserParams: (values: Record<string, string>) => Promise<void>;
|
||||
stopStreaming: () => void;
|
||||
sessionLoadError?: string;
|
||||
@@ -89,7 +94,12 @@ async function streamFromResponse(
|
||||
(content) => content.type === 'toolConfirmationRequest'
|
||||
);
|
||||
|
||||
if (hasToolConfirmation) {
|
||||
const hasElicitation = msg.content.some(
|
||||
(content) =>
|
||||
content.type === 'actionRequired' && content.data.actionType === 'elicitation'
|
||||
);
|
||||
|
||||
if (hasToolConfirmation || hasElicitation) {
|
||||
updateChatState(ChatState.WaitingForUserInput);
|
||||
} else if (getCompactingMessage(msg)) {
|
||||
updateChatState(ChatState.Compacting);
|
||||
@@ -312,6 +322,50 @@ export function useChatStream({
|
||||
[sessionId, session, chatState, updateMessages, updateNotifications, onFinish]
|
||||
);
|
||||
|
||||
const submitElicitationResponse = useCallback(
|
||||
async (elicitationId: string, userData: Record<string, unknown>) => {
|
||||
if (!session || chatState === ChatState.LoadingConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const responseMessage = createElicitationResponseMessage(elicitationId, userData);
|
||||
const currentMessages = [...messagesRef.current, responseMessage];
|
||||
|
||||
updateMessages(currentMessages);
|
||||
setChatState(ChatState.Streaming);
|
||||
setNotifications([]);
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
const { stream } = await reply({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
messages: currentMessages,
|
||||
},
|
||||
throwOnError: true,
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
|
||||
await streamFromResponse(
|
||||
stream,
|
||||
currentMessages,
|
||||
updateMessages,
|
||||
setTokenState,
|
||||
setChatState,
|
||||
updateNotifications,
|
||||
onFinish
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
// Silently handle abort
|
||||
} else {
|
||||
onFinish('Submit error: ' + errorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
[sessionId, session, chatState, updateMessages, updateNotifications, onFinish]
|
||||
);
|
||||
|
||||
const setRecipeUserParams = useCallback(
|
||||
async (user_recipe_values: Record<string, string>) => {
|
||||
if (session) {
|
||||
@@ -437,6 +491,7 @@ export function useChatStream({
|
||||
session: maybe_cached_session,
|
||||
chatState,
|
||||
handleSubmit,
|
||||
submitElicitationResponse,
|
||||
stopStreaming,
|
||||
setRecipeUserParams,
|
||||
tokenState,
|
||||
|
||||
@@ -17,6 +17,28 @@ export function createUserMessage(text: string): Message {
|
||||
};
|
||||
}
|
||||
|
||||
export function createElicitationResponseMessage(
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
): Message {
|
||||
return {
|
||||
id: generateMessageId(),
|
||||
role: 'user',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
content: [
|
||||
{
|
||||
type: 'actionRequired',
|
||||
data: {
|
||||
actionType: 'elicitationResponse',
|
||||
id: elicitationId,
|
||||
user_data: userData,
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: { userVisible: false, agentVisible: true },
|
||||
};
|
||||
}
|
||||
|
||||
export function generateMessageId(): string {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
@@ -51,6 +73,15 @@ export function getToolConfirmationContent(
|
||||
);
|
||||
}
|
||||
|
||||
export function getElicitationContent(
|
||||
message: Message
|
||||
): (ActionRequired & { type: 'actionRequired' }) | undefined {
|
||||
return message.content.find(
|
||||
(content): content is ActionRequired & { type: 'actionRequired' } =>
|
||||
content.type === 'actionRequired' && content.data.actionType === 'elicitation'
|
||||
);
|
||||
}
|
||||
|
||||
export function hasCompletedToolCalls(message: Message): boolean {
|
||||
const toolRequests = getToolRequests(message);
|
||||
return toolRequests.length > 0;
|
||||
|
||||
Reference in New Issue
Block a user