import { NgComponentOutlet } from "@angular/common";
import { Component, inject, input } from "@angular/core";
import {
AssistantMessage,
Message,
ToolCall,
ToolMessage,
} from "@ag-ui/client";
import { CopilotKit } from "./copilotkit";
import {
FrontendToolConfig,
HumanInTheLoopToolCall,
HumanInTheLoopConfig,
AngularToolCall,
RenderToolCallConfig,
} from "./tools";
import { partialJSONParse } from "@copilotkit/shared";
import { HumanInTheLoop } from "./human-in-the-loop";
type RendererToolCallHandler = {
type: "renderer";
config: RenderToolCallConfig;
};
type ClientToolCallHandler = {
type: "clientTool";
config: FrontendToolConfig;
};
type HumanInTheLoopToolCallHandler = {
type: "humanInTheLoopTool";
config: HumanInTheLoopConfig;
};
type ToolCallHandler =
| RendererToolCallHandler
| ClientToolCallHandler
| HumanInTheLoopToolCallHandler;
@Component({
selector: "copilot-render-tool-calls",
standalone: true,
imports: [NgComponentOutlet],
template: `
@for (toolCall of message().toolCalls ?? []; track toolCall.id) {
@let renderConfig = pickRenderer(toolCall.function.name);
@if (
renderConfig &&
renderConfig.type !== "humanInTheLoopTool" &&
renderConfig.config.component
) {
}
@if (
renderConfig &&
renderConfig.type === "humanInTheLoopTool" &&
renderConfig.config.component
) {
}
}
`,
})
export class RenderToolCalls {
readonly #copilotKit = inject(CopilotKit);
readonly #hitl = inject(HumanInTheLoop);
readonly message = input.required();
readonly messages = input.required();
readonly isLoading = input(false);
protected pickRenderer(name: string): ToolCallHandler | undefined {
type AssistantMessageWithAgent = AssistantMessage & {
agentId?: string;
};
const messageAgentId = (this.message() as AssistantMessageWithAgent)
.agentId;
const renderers = this.#copilotKit.toolCallRenderConfigs();
const clientTools = this.#copilotKit.clientToolCallRenderConfigs();
const humanInTheLoopTools =
this.#copilotKit.humanInTheLoopToolRenderConfigs();
const renderer = renderers.find(
(candidate) =>
candidate.name === name &&
(candidate.agentId === undefined ||
candidate.agentId === messageAgentId),
);
if (renderer) return { type: "renderer", config: renderer };
const clientTool = clientTools.find(
(candidate) =>
candidate.name === name &&
(candidate.agentId === undefined ||
candidate.agentId === messageAgentId),
);
if (clientTool) return { type: "clientTool", config: clientTool };
const humanInTheLoopTool = humanInTheLoopTools.find(
(candidate) =>
candidate.name === name &&
(candidate.agentId === undefined ||
candidate.agentId === messageAgentId),
);
if (humanInTheLoopTool)
return { type: "humanInTheLoopTool", config: humanInTheLoopTool };
const starRenderer = renderers.find((candidate) => candidate.name === "*");
if (starRenderer) return { type: "renderer", config: starRenderer };
return undefined;
}
protected buildToolCall>(
toolCall: ToolCall,
): AngularToolCall {
const args = partialJSONParse(toolCall.function.arguments) as Args;
const message = this.#getToolMessage(toolCall.id);
if (message) {
return {
args,
status: "complete",
result: message.content,
};
} else if (this.isLoading()) {
return {
args,
status: "in-progress",
result: undefined,
};
} else {
return {
args,
status: "executing",
result: undefined,
};
}
}
protected buildHumanInTheLoopToolCall>(
toolCall: ToolCall,
): HumanInTheLoopToolCall {
const args = partialJSONParse(toolCall.function.arguments) as Args;
const message = this.#getToolMessage(toolCall.id);
const respond = (result: unknown) => {
this.#hitl.addResult(toolCall.id, toolCall.function.name, result);
};
if (message) {
return {
args,
status: "complete",
result: message.content!,
respond,
};
} else if (this.isLoading()) {
return {
args,
status: "in-progress",
result: undefined,
respond,
};
} else {
return {
args,
status: "executing",
result: undefined,
respond,
};
}
}
#getToolMessage(toolCallId: string): ToolMessage | undefined {
const message = this.messages().find(
(m): m is ToolMessage => m.role === "tool" && m.toolCallId === toolCallId,
);
return message;
}
}