diff --git a/.changeset/wise-pans-arrive.md b/.changeset/wise-pans-arrive.md index 6d149c1f1..97b8527e0 100644 --- a/.changeset/wise-pans-arrive.md +++ b/.changeset/wise-pans-arrive.md @@ -5,5 +5,5 @@ fix: preserve assistant feedback metadata across AG-UI streams - Map VoltAgent `message-metadata` stream chunks to AG-UI `CUSTOM` events, which are the protocol-native channel for application-specific metadata. -- Keep emitting a legacy internal tool-result marker for backward compatibility with existing clients. -- Prevent internal metadata marker tool messages from being sent back to the model on subsequent turns. +- Stop emitting legacy internal tool-result metadata markers from the adapter. +- Remove the legacy metadata marker filter from model-input message conversion. diff --git a/packages/ag-ui/src/voltagent-agent.ts b/packages/ag-ui/src/voltagent-agent.ts index 721d05f2d..a49d91e16 100644 --- a/packages/ag-ui/src/voltagent-agent.ts +++ b/packages/ag-ui/src/voltagent-agent.ts @@ -275,15 +275,7 @@ type VoltUIMessage = { parts?: VoltUIPart[]; }; -const VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX = "__voltagent_message_metadata__:"; const VOLTAGENT_MESSAGE_METADATA_EVENT_NAME = "voltagent.message_metadata"; - -function isMetadataCarrierToolCallId(toolCallId: string | undefined): boolean { - return ( - typeof toolCallId === "string" && toolCallId.startsWith(VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX) - ); -} - function convertAGUIMessagesToVoltMessages(messages: Message[]): VoltUIMessage[] { const toolNameById = new Map(); const convertedMessages: VoltUIMessage[] = []; @@ -336,9 +328,6 @@ function convertAGUIMessagesToVoltMessages(messages: Message[]): VoltUIMessage[] } if (isToolMessage(msg)) { - if (isMetadataCarrierToolCallId(msg.toolCallId)) { - continue; - } const toolName = msg.toolCallId ? toolNameById.get(msg.toolCallId) : undefined; convertedMessages.push({ id: messageId, @@ -435,18 +424,7 @@ function convertVoltStreamPartToEvents( }, }; - // Backward compatibility for existing clients that consume metadata from tool messages. - const resultEvent: ToolCallResultEvent = { - type: EventType.TOOL_CALL_RESULT, - toolCallId: `${VOLTAGENT_METADATA_TOOL_CALL_ID_PREFIX}${messageId || generateId()}`, - content: safeStringify({ - messageId: messageId || undefined, - metadata: messageMetadata, - }), - messageId: generateId(), - role: "tool", - }; - return [customEvent, resultEvent]; + return [customEvent]; } switch (part.type) { diff --git a/website/docs/ui/copilotkit.md b/website/docs/ui/copilotkit.md index 112c14611..ac533cc93 100644 --- a/website/docs/ui/copilotkit.md +++ b/website/docs/ui/copilotkit.md @@ -197,6 +197,163 @@ export default function App() { } ``` +## CopilotKit feedback -> VoltOps feedback (recommended) + +If you want CopilotKit thumbs up/down to persist as VoltOps trace feedback, wire them explicitly. + +### 1. Enable feedback on the agent + +```ts title="examples/with-copilotkit/server/src/index.ts" +const mathAgent = new Agent({ + name: "MathAgent", + instructions: "You are a concise math tutor.", + model: "openai/gpt-4o-mini", + feedback: { + key: "satisfaction", + feedbackConfig: { + type: "categorical", + categories: [ + { value: 1, label: "Satisfied" }, + { value: 0, label: "Unsatisfied" }, + ], + }, + }, +}); +``` + +### 2. Capture message metadata + submit thumbs feedback + +`@voltagent/ag-ui` emits message metadata as AG-UI `CUSTOM` events with name `voltagent.message_metadata`. +Use that payload to map each assistant `messageId` to its VoltOps feedback URL, then call the URL in `onThumbsUp` / `onThumbsDown`. + +```tsx title="examples/with-copilotkit/client/src/App.tsx" +import { useEffect, useMemo, useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { CopilotKit, useCopilotChatInternal } from "@copilotkit/react-core"; +import { CopilotChat } from "@copilotkit/react-ui"; +import type { Message } from "@copilotkit/shared"; +import { safeStringify } from "@voltagent/internal"; +import "@copilotkit/react-ui/styles.css"; + +type VoltFeedbackMetadata = { + url?: string; + provided?: boolean; + providedAt?: string; + feedbackId?: string; +}; + +const VOLTAGENT_MESSAGE_METADATA_EVENT_NAME = "voltagent.message_metadata"; + +function useVoltFeedbackMap() { + const { agent } = useCopilotChatInternal(); + const [feedbackByMessageId, setFeedbackByMessageId] = useState< + Record + >({}); + + useEffect(() => { + if (!agent) return; + + const subscription = agent.subscribe({ + onCustomEvent: ({ event }) => { + if (event.name !== VOLTAGENT_MESSAGE_METADATA_EVENT_NAME) return; + + const payload = event.value as + | { + messageId?: string; + metadata?: { + feedback?: VoltFeedbackMetadata; + }; + } + | undefined; + + const messageId = payload?.messageId; + const feedback = payload?.metadata?.feedback; + + if (!messageId || !feedback?.url) return; + setFeedbackByMessageId((prev) => ({ ...prev, [messageId]: feedback })); + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }, [agent]); + + return { feedbackByMessageId, setFeedbackByMessageId }; +} + +function isProvided(feedback?: VoltFeedbackMetadata): boolean { + return Boolean(feedback?.provided || feedback?.providedAt || feedback?.feedbackId); +} + +async function submitVoltFeedback(input: { + message: Message; + type: "thumbsUp" | "thumbsDown"; + feedbackByMessageId: Record; + setFeedbackByMessageId: Dispatch>>; +}) { + const feedback = input.feedbackByMessageId[input.message.id]; + if (!feedback?.url || isProvided(feedback)) return; + + const score = input.type === "thumbsUp" ? 1 : 0; + + const response = await fetch(feedback.url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: safeStringify({ + score, + feedback_source_type: "app", + }), + }); + + if (!response.ok) return; + + const data = (await response.json()) as { id?: string }; + setFeedbackByMessageId((prev) => ({ + ...prev, + [input.message.id]: { + ...feedback, + provided: true, + feedbackId: data.id, + }, + })); +} + +function ChatWithFeedback() { + const runtimeUrl = useMemo( + () => import.meta.env.VITE_RUNTIME_URL || "http://localhost:3141/copilotkit", + [] + ); + const { feedbackByMessageId, setFeedbackByMessageId } = useVoltFeedbackMap(); + + return ( + + + void submitVoltFeedback({ + message, + type: "thumbsUp", + feedbackByMessageId, + setFeedbackByMessageId, + }) + } + onThumbsDown={(message) => + void submitVoltFeedback({ + message, + type: "thumbsDown", + feedbackByMessageId, + setFeedbackByMessageId, + }) + } + /> + + ); +} +``` + +For the complete lifecycle (including persisting `provided` state in memory with `agent.markFeedbackProvided(...)`), see [Feedback](/observability-docs/feedback). + ## Tips - CopilotKit DevTools lets you switch agents when multiple are exposed (e.g., `MathAgent` vs `StoryAgent`).