mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
feat(react-core): expose raw event metadata to feedback callbacks
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@copilotkit/core": minor
|
||||
"@copilotkit/react-core": minor
|
||||
---
|
||||
|
||||
feat(react-core): expose raw event metadata to feedback callbacks
|
||||
@@ -0,0 +1,248 @@
|
||||
import { AbstractAgent, EventType, transformChunks } from "@ag-ui/client";
|
||||
import type { BaseEvent, RunAgentInput } from "@ag-ui/client";
|
||||
import { firstValueFrom, of, toArray } from "rxjs";
|
||||
import type { Observable } from "rxjs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CopilotKitCore } from "../core";
|
||||
|
||||
class RawEventAgent extends AbstractAgent {
|
||||
readonly inputs: RunAgentInput[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly eventFactory: (input: RunAgentInput) => BaseEvent[],
|
||||
agentId = "raw-event-agent",
|
||||
threadId = "raw-event-thread",
|
||||
) {
|
||||
super({ agentId, threadId });
|
||||
}
|
||||
|
||||
run(input: RunAgentInput): Observable<BaseEvent> {
|
||||
this.inputs.push(input);
|
||||
return of(...this.eventFactory(input));
|
||||
}
|
||||
}
|
||||
|
||||
function runEvents(input: RunAgentInput): BaseEvent[] {
|
||||
return [
|
||||
{
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
{
|
||||
type: EventType.MESSAGES_SNAPSHOT,
|
||||
rawEvent: { source: "snapshot" },
|
||||
messages: [{ id: "snapshot-user", role: "user", content: "hello" }],
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId: "assistant-raw-event",
|
||||
role: "assistant",
|
||||
rawEvent: { langfuse_trace_id: "trace-3039" },
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId: "assistant-raw-event",
|
||||
delta: "answer",
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId: "assistant-raw-event",
|
||||
},
|
||||
{
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe("StateManager direct text-start raw event sidecar", () => {
|
||||
it("captures scoped metadata without changing canonical or outbound messages", async () => {
|
||||
const agent = new RawEventAgent(runEvents);
|
||||
const otherAgent = new RawEventAgent(
|
||||
(input) => runEvents(input),
|
||||
"other-agent",
|
||||
"other-thread",
|
||||
);
|
||||
const core = new CopilotKitCore({
|
||||
agents__unsafe_dev_only: {
|
||||
[agent.agentId!]: agent,
|
||||
[otherAgent.agentId!]: otherAgent,
|
||||
},
|
||||
});
|
||||
let messageChanges = 0;
|
||||
agent.subscribe({
|
||||
onMessagesChanged: () => {
|
||||
messageChanges++;
|
||||
},
|
||||
});
|
||||
|
||||
await agent.runAgent({ runId: "raw-event-run" });
|
||||
|
||||
expect(messageChanges).toBe(3);
|
||||
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
"raw-event-agent",
|
||||
"raw-event-thread",
|
||||
"assistant-raw-event",
|
||||
),
|
||||
).toEqual({ langfuse_trace_id: "trace-3039" });
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
"raw-event-agent",
|
||||
"other-thread",
|
||||
"assistant-raw-event",
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
"other-agent",
|
||||
"other-thread",
|
||||
"assistant-raw-event",
|
||||
),
|
||||
).toBeUndefined();
|
||||
|
||||
for (const message of agent.messages) {
|
||||
expect(Object.prototype.hasOwnProperty.call(message, "rawEvent")).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
await agent.runAgent({ runId: "raw-event-run-2" });
|
||||
for (const message of agent.inputs[1]?.messages ?? []) {
|
||||
expect(Object.prototype.hasOwnProperty.call(message, "rawEvent")).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves false, 0, empty string, and null while ignoring undefined", async () => {
|
||||
const values: unknown[] = [false, 0, "", null, undefined];
|
||||
const agent = new RawEventAgent((input) => [
|
||||
{
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
...values.flatMap((rawEvent, index) => [
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId: `boundary-${index}`,
|
||||
role: "assistant",
|
||||
...(rawEvent === undefined ? {} : { rawEvent }),
|
||||
} as BaseEvent,
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId: `boundary-${index}`,
|
||||
} as BaseEvent,
|
||||
]),
|
||||
{
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
]);
|
||||
const core = new CopilotKitCore({
|
||||
agents__unsafe_dev_only: { [agent.agentId!]: agent },
|
||||
});
|
||||
|
||||
await agent.runAgent({ runId: "boundary-run" });
|
||||
|
||||
expect(
|
||||
[0, 1, 2, 3].map((index) =>
|
||||
core.getRawEventForMessage(
|
||||
agent.agentId!,
|
||||
agent.threadId,
|
||||
`boundary-${index}`,
|
||||
),
|
||||
),
|
||||
).toEqual([false, 0, "", null]);
|
||||
expect(
|
||||
core.getRawEventForMessage(agent.agentId!, agent.threadId, "boundary-4"),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prunes removed messages and leaves snapshot metadata outside the sidecar", async () => {
|
||||
const agent = new RawEventAgent((input) => [
|
||||
{
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
{
|
||||
type: EventType.MESSAGES_SNAPSHOT,
|
||||
rawEvent: { source: "snapshot" },
|
||||
messages: [{ id: "snapshot-only", role: "user", content: "old" }],
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId: "pruned-message",
|
||||
role: "assistant",
|
||||
rawEvent: { source: "direct" },
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId: "pruned-message",
|
||||
},
|
||||
{
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
]);
|
||||
const core = new CopilotKitCore({
|
||||
agents__unsafe_dev_only: { [agent.agentId!]: agent },
|
||||
});
|
||||
|
||||
await agent.runAgent({ runId: "prune-run" });
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
agent.agentId!,
|
||||
agent.threadId,
|
||||
"pruned-message",
|
||||
),
|
||||
).toEqual({ source: "direct" });
|
||||
|
||||
agent.setMessages([]);
|
||||
await Promise.resolve();
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
agent.agentId!,
|
||||
agent.threadId,
|
||||
"pruned-message",
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
core.getRawEventForMessage(
|
||||
agent.agentId!,
|
||||
agent.threadId,
|
||||
"snapshot-only",
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves TEXT_MESSAGE_CHUNK rawEvent outside the normalized start event", async () => {
|
||||
const normalized = await firstValueFrom(
|
||||
transformChunks()(
|
||||
of({
|
||||
type: EventType.TEXT_MESSAGE_CHUNK,
|
||||
messageId: "chunk-message",
|
||||
delta: "chunk",
|
||||
rawEvent: { source: "chunk" },
|
||||
}),
|
||||
).pipe(toArray()),
|
||||
);
|
||||
|
||||
expect(
|
||||
normalized.some((event) => event.type === EventType.TEXT_MESSAGE_CHUNK),
|
||||
).toBe(false);
|
||||
expect(
|
||||
normalized.find((event) => event.type === EventType.TEXT_MESSAGE_START),
|
||||
).toMatchObject({ messageId: "chunk-message" });
|
||||
expect(
|
||||
normalized.find((event) => event.type === EventType.TEXT_MESSAGE_START),
|
||||
).not.toHaveProperty("rawEvent");
|
||||
});
|
||||
});
|
||||
@@ -528,6 +528,10 @@ export class CopilotKitCore {
|
||||
this.previousAgentIds = currentAgentIds;
|
||||
},
|
||||
});
|
||||
|
||||
Object.values(agents__unsafe_dev_only).forEach((agent) => {
|
||||
if (agent.agentId) this.stateManager.subscribeToAgent(agent);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1327,6 +1331,18 @@ export class CopilotKitCore {
|
||||
return this.stateManager.getRunIdForMessage(agentId, threadId, messageId);
|
||||
}
|
||||
|
||||
getRawEventForMessage(
|
||||
agentId: string,
|
||||
threadId: string,
|
||||
messageId: string,
|
||||
): unknown {
|
||||
return this.stateManager.getRawEventForMessage(
|
||||
agentId,
|
||||
threadId,
|
||||
messageId,
|
||||
);
|
||||
}
|
||||
|
||||
getRunIdsForThread(agentId: string, threadId: string): string[] {
|
||||
return this.stateManager.getRunIdsForThread(agentId, threadId);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
StateSnapshotEvent,
|
||||
StateDeltaEvent,
|
||||
MessagesSnapshotEvent,
|
||||
TextMessageStartEvent,
|
||||
} from "@ag-ui/client";
|
||||
import { randomUUID } from "@ag-ui/client";
|
||||
import type { CopilotKitCore } from "./core";
|
||||
@@ -22,6 +23,10 @@ export class StateManager {
|
||||
private messageToRun: Map<string, Map<string, Map<string, string>>> =
|
||||
new Map();
|
||||
|
||||
// Direct text-start metadata: agentId -> threadId -> messageId -> rawEvent
|
||||
private rawEventByMessage: Map<string, Map<string, Map<string, unknown>>> =
|
||||
new Map();
|
||||
|
||||
// Active run tracking: `agentId:threadId` -> runId (used when messages arrive without input)
|
||||
private activeRun: Map<string, string> = new Map();
|
||||
|
||||
@@ -117,6 +122,10 @@ export class StateManager {
|
||||
if (revoked) return;
|
||||
this.handleStateDelta(agent, event, effectiveInput(input), state);
|
||||
},
|
||||
onTextMessageStartEvent: ({ event, input }) => {
|
||||
if (revoked) return;
|
||||
this.handleTextMessageStart(agent, event, effectiveInput(input));
|
||||
},
|
||||
onMessagesSnapshotEvent: ({ event, input, messages }) => {
|
||||
if (revoked) return;
|
||||
this.handleMessagesSnapshot(
|
||||
@@ -125,6 +134,12 @@ export class StateManager {
|
||||
effectiveInput(input),
|
||||
messages,
|
||||
);
|
||||
this.pruneRawEvents(
|
||||
agent.agentId!,
|
||||
input.threadId,
|
||||
event.messages,
|
||||
effectiveInput(input),
|
||||
);
|
||||
},
|
||||
onNewMessage: ({ message, input }) => {
|
||||
if (revoked) return;
|
||||
@@ -134,6 +149,12 @@ export class StateManager {
|
||||
input ? effectiveInput(input) : undefined,
|
||||
);
|
||||
},
|
||||
onMessagesChanged: ({ messages, input }) => {
|
||||
if (revoked) return;
|
||||
if (!input) {
|
||||
this.pruneRawEvents(agent.agentId!, agent.threadId, messages);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.agentSubscriptions.set(agentId, () => {
|
||||
@@ -151,6 +172,7 @@ export class StateManager {
|
||||
unsubscribe();
|
||||
this.agentSubscriptions.delete(agentId);
|
||||
}
|
||||
this.clearAgentState(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +201,17 @@ export class StateManager {
|
||||
return this.messageToRun.get(agentId)?.get(threadId)?.get(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get direct text-start metadata associated with a message.
|
||||
*/
|
||||
getRawEventForMessage(
|
||||
agentId: string,
|
||||
threadId: string,
|
||||
messageId: string,
|
||||
): unknown {
|
||||
return this.rawEventByMessage.get(agentId)?.get(threadId)?.get(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all states for an agent's thread
|
||||
*/
|
||||
@@ -265,6 +298,27 @@ export class StateManager {
|
||||
this.saveState(agent.agentId, threadId, runId, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture only defined metadata from a normalized direct text-start event.
|
||||
*/
|
||||
private handleTextMessageStart(
|
||||
agent: AbstractAgent,
|
||||
event: TextMessageStartEvent,
|
||||
input: RunAgentInput,
|
||||
): void {
|
||||
if (!agent.agentId || event.rawEvent === undefined) return;
|
||||
|
||||
const { threadId } = input;
|
||||
if (!this.rawEventByMessage.has(agent.agentId)) {
|
||||
this.rawEventByMessage.set(agent.agentId, new Map());
|
||||
}
|
||||
const agentEvents = this.rawEventByMessage.get(agent.agentId)!;
|
||||
if (!agentEvents.has(threadId)) {
|
||||
agentEvents.set(threadId, new Map());
|
||||
}
|
||||
agentEvents.get(threadId)!.set(event.messageId, event.rawEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages snapshot event
|
||||
*/
|
||||
@@ -272,7 +326,7 @@ export class StateManager {
|
||||
agent: AbstractAgent,
|
||||
event: MessagesSnapshotEvent,
|
||||
input: RunAgentInput,
|
||||
messages: readonly Message[],
|
||||
_messages: readonly Message[],
|
||||
): void {
|
||||
if (!agent.agentId) return;
|
||||
|
||||
@@ -361,12 +415,37 @@ export class StateManager {
|
||||
threadMessages.set(messageId, runId);
|
||||
}
|
||||
|
||||
private pruneRawEvents(
|
||||
agentId: string,
|
||||
fallbackThreadId: string | undefined,
|
||||
messages: ReadonlyArray<Readonly<Message>>,
|
||||
input?: RunAgentInput,
|
||||
): void {
|
||||
const threadId = input?.threadId ?? fallbackThreadId;
|
||||
if (!threadId) return;
|
||||
|
||||
const threadEvents = this.rawEventByMessage.get(agentId)?.get(threadId);
|
||||
if (!threadEvents) return;
|
||||
|
||||
const messageIds = new Set(messages.map((message) => message.id));
|
||||
for (const messageId of threadEvents.keys()) {
|
||||
if (!messageIds.has(messageId)) threadEvents.delete(messageId);
|
||||
}
|
||||
if (threadEvents.size === 0) {
|
||||
this.rawEventByMessage.get(agentId)?.delete(threadId);
|
||||
}
|
||||
if (this.rawEventByMessage.get(agentId)?.size === 0) {
|
||||
this.rawEventByMessage.delete(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all state for an agent
|
||||
*/
|
||||
clearAgentState(agentId: string): void {
|
||||
this.stateByRun.delete(agentId);
|
||||
this.messageToRun.delete(agentId);
|
||||
this.rawEventByMessage.delete(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,5 +454,6 @@ export class StateManager {
|
||||
clearThreadState(agentId: string, threadId: string): void {
|
||||
this.stateByRun.get(agentId)?.delete(threadId);
|
||||
this.messageToRun.get(agentId)?.delete(threadId);
|
||||
this.rawEventByMessage.get(agentId)?.delete(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,3 +106,12 @@ your task — do not try to absorb the whole package from this file.
|
||||
3. `agent-access` — talk to agents.
|
||||
4. `client-side-tools` + `rendering-tool-calls` — add tool-call UI.
|
||||
5. Anything else as your feature requires.
|
||||
|
||||
## Feedback metadata
|
||||
|
||||
React v2 thumbs callbacks receive `CopilotChatFeedbackMessage`. For a live
|
||||
assistant message created by a direct AG-UI `TEXT_MESSAGE_START` event, the
|
||||
callback argument can include that event's opaque `rawEvent` value. The value
|
||||
is joined at click time and stays out of canonical messages, rendering props,
|
||||
and future run input. Chunk, snapshot, persisted, and legacy message paths do
|
||||
not provide this metadata.
|
||||
|
||||
@@ -97,6 +97,27 @@ export function HeadlessChat() {
|
||||
/>
|
||||
```
|
||||
|
||||
### Feedback event metadata
|
||||
|
||||
Pass thumbs callbacks through the `assistantMessage` slot when a live direct
|
||||
AG-UI text-start event's metadata is needed:
|
||||
|
||||
```tsx
|
||||
<CopilotChatMessageView
|
||||
messages={agent.messages}
|
||||
assistantMessage={{
|
||||
onThumbsUp: (message) => {
|
||||
console.log(message.rawEvent);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
The callback receives the same assistant message ID and a callback-only
|
||||
`rawEvent` value when the message came from a direct `TEXT_MESSAGE_START`.
|
||||
Canonical messages and future run input remain protocol-clean. Chunk and
|
||||
snapshot metadata aren't attributed to feedback messages.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### CRITICAL — Importing `CopilotPanel`
|
||||
|
||||
@@ -26,6 +26,10 @@ import { Streamdown } from "streamdown";
|
||||
import { copyToClipboard } from "@copilotkit/shared";
|
||||
import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
|
||||
|
||||
export type CopilotChatFeedbackMessage = AssistantMessage & {
|
||||
rawEvent?: unknown;
|
||||
};
|
||||
|
||||
export type CopilotChatAssistantMessageProps = WithSlots<
|
||||
{
|
||||
markdownRenderer: typeof CopilotChatAssistantMessage.MarkdownRenderer;
|
||||
@@ -38,8 +42,8 @@ export type CopilotChatAssistantMessageProps = WithSlots<
|
||||
toolCallsView: typeof CopilotChatToolCallsView;
|
||||
},
|
||||
{
|
||||
onThumbsUp?: (message: AssistantMessage) => void;
|
||||
onThumbsDown?: (message: AssistantMessage) => void;
|
||||
onThumbsUp?: (message: CopilotChatFeedbackMessage) => void;
|
||||
onThumbsDown?: (message: CopilotChatFeedbackMessage) => void;
|
||||
onReadAloud?: (message: AssistantMessage) => void;
|
||||
onRegenerate?: (message: AssistantMessage) => void;
|
||||
message: AssistantMessage;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ScrollElementContext } from "./scroll-element-context";
|
||||
import type { WithSlots } from "../../lib/slots";
|
||||
import { renderSlot, isReactComponentType } from "../../lib/slots";
|
||||
import CopilotChatAssistantMessage from "./CopilotChatAssistantMessage";
|
||||
import type { CopilotChatFeedbackMessage } from "./CopilotChatAssistantMessage";
|
||||
import CopilotChatUserMessage from "./CopilotChatUserMessage";
|
||||
import CopilotChatReasoningMessage from "./CopilotChatReasoningMessage";
|
||||
import type {
|
||||
@@ -465,6 +466,40 @@ export function CopilotChatMessageView({
|
||||
() => resolveSlotComponent(assistantMessage, CopilotChatAssistantMessage),
|
||||
[assistantMessage],
|
||||
);
|
||||
const assistantSlotPropsWithFeedback = useMemo(() => {
|
||||
const onThumbsUp = assistantSlotProps?.onThumbsUp as
|
||||
| ((message: CopilotChatFeedbackMessage) => void)
|
||||
| undefined;
|
||||
const onThumbsDown = assistantSlotProps?.onThumbsDown as
|
||||
| ((message: CopilotChatFeedbackMessage) => void)
|
||||
| undefined;
|
||||
if (!onThumbsUp && !onThumbsDown) return assistantSlotProps;
|
||||
|
||||
const withRawEvent = (
|
||||
message: AssistantMessage,
|
||||
): CopilotChatFeedbackMessage => {
|
||||
const rawEvent = config
|
||||
? copilotkit.getRawEventForMessage(
|
||||
config.agentId,
|
||||
config.threadId,
|
||||
message.id,
|
||||
)
|
||||
: undefined;
|
||||
return rawEvent === undefined ? message : { ...message, rawEvent };
|
||||
};
|
||||
|
||||
return {
|
||||
...assistantSlotProps,
|
||||
...(onThumbsUp && {
|
||||
onThumbsUp: (message: AssistantMessage) =>
|
||||
onThumbsUp(withRawEvent(message)),
|
||||
}),
|
||||
...(onThumbsDown && {
|
||||
onThumbsDown: (message: AssistantMessage) =>
|
||||
onThumbsDown(withRawEvent(message)),
|
||||
}),
|
||||
};
|
||||
}, [assistantSlotProps, config, copilotkit]);
|
||||
const { Component: UserComponent, slotProps: userSlotProps } = useMemo(
|
||||
() => resolveSlotComponent(userMessage, CopilotChatUserMessage),
|
||||
[userMessage],
|
||||
@@ -578,7 +613,7 @@ export function CopilotChatMessageView({
|
||||
messages={messages}
|
||||
isRunning={isRunning}
|
||||
AssistantMessageComponent={AssistantComponent}
|
||||
slotProps={assistantSlotProps}
|
||||
slotProps={assistantSlotPropsWithFeedback}
|
||||
/>,
|
||||
);
|
||||
} else if (message.role === "user") {
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { AbstractAgent, EventType } from "@ag-ui/client";
|
||||
import type { BaseEvent, RunAgentInput } from "@ag-ui/client";
|
||||
import { of } from "rxjs";
|
||||
import type { Observable } from "rxjs";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
||||
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
||||
import { CopilotChatMessageView } from "../CopilotChatMessageView";
|
||||
|
||||
class FeedbackAgent extends AbstractAgent {
|
||||
readonly inputs: RunAgentInput[] = [];
|
||||
readonly rawEvent = { langfuse_trace_id: "trace-3039" };
|
||||
|
||||
constructor() {
|
||||
super({ agentId: "feedback-agent", threadId: "feedback-thread" });
|
||||
}
|
||||
|
||||
run(input: RunAgentInput): Observable<BaseEvent> {
|
||||
this.inputs.push(input);
|
||||
const messageId = `assistant-${this.inputs.length}`;
|
||||
return of(
|
||||
{
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId,
|
||||
role: "assistant",
|
||||
rawEvent: this.rawEvent,
|
||||
},
|
||||
{
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId,
|
||||
delta: "Answer",
|
||||
},
|
||||
{ type: EventType.TEXT_MESSAGE_END, messageId },
|
||||
{
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId: input.threadId,
|
||||
runId: input.runId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe("CopilotChatMessageView feedback raw event", () => {
|
||||
it("joins direct start metadata at the real thumbs callback boundary", async () => {
|
||||
const agent = new FeedbackAgent();
|
||||
const onThumbsUp = vi.fn();
|
||||
const onThumbsDown = vi.fn();
|
||||
const view = render(
|
||||
<CopilotKitProvider agents__unsafe_dev_only={{ "feedback-agent": agent }}>
|
||||
<CopilotChatConfigurationProvider
|
||||
agentId="feedback-agent"
|
||||
threadId="feedback-thread"
|
||||
>
|
||||
<CopilotChatMessageView
|
||||
messages={agent.messages}
|
||||
assistantMessage={{ onThumbsUp, onThumbsDown }}
|
||||
/>
|
||||
</CopilotChatConfigurationProvider>
|
||||
</CopilotKitProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await agent.runAgent({ runId: "feedback-run-1" });
|
||||
});
|
||||
expect(agent.rawEvent).toEqual({ langfuse_trace_id: "trace-3039" });
|
||||
view.rerender(
|
||||
<CopilotKitProvider agents__unsafe_dev_only={{ "feedback-agent": agent }}>
|
||||
<CopilotChatConfigurationProvider
|
||||
agentId="feedback-agent"
|
||||
threadId="feedback-thread"
|
||||
>
|
||||
<CopilotChatMessageView
|
||||
messages={agent.messages}
|
||||
assistantMessage={{ onThumbsUp, onThumbsDown }}
|
||||
/>
|
||||
</CopilotChatConfigurationProvider>
|
||||
</CopilotKitProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /good response/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /bad response/i }));
|
||||
|
||||
expect(onThumbsUp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "assistant-1",
|
||||
rawEvent: { langfuse_trace_id: "trace-3039" },
|
||||
}),
|
||||
);
|
||||
expect(onThumbsDown).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "assistant-1",
|
||||
rawEvent: { langfuse_trace_id: "trace-3039" },
|
||||
}),
|
||||
);
|
||||
expect(onThumbsUp.mock.calls[0]?.[0]).not.toBe(agent.messages[0]);
|
||||
for (const message of agent.messages) {
|
||||
expect(Object.prototype.hasOwnProperty.call(message, "rawEvent")).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
await agent.runAgent({ runId: "feedback-run-2" });
|
||||
});
|
||||
expect(
|
||||
agent.inputs[1]?.messages.some((message) =>
|
||||
Object.prototype.hasOwnProperty.call(message, "rawEvent"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ export {
|
||||
export {
|
||||
default as CopilotChatAssistantMessage,
|
||||
type CopilotChatAssistantMessageProps,
|
||||
type CopilotChatFeedbackMessage,
|
||||
} from "./CopilotChatAssistantMessage";
|
||||
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user