mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 11:02:12 +03:00
refactor(gateway): share agent prompt builder
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildHistoryContextFromEntries } from "../auto-reply/reply/history.js";
|
||||||
|
import { buildAgentMessageFromConversationEntries } from "./agent-prompt.js";
|
||||||
|
|
||||||
|
describe("gateway agent prompt", () => {
|
||||||
|
it("returns empty for no entries", () => {
|
||||||
|
expect(buildAgentMessageFromConversationEntries([])).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns current body when there is no history", () => {
|
||||||
|
expect(
|
||||||
|
buildAgentMessageFromConversationEntries([
|
||||||
|
{ role: "user", entry: { sender: "User", body: "hi" } },
|
||||||
|
]),
|
||||||
|
).toBe("hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses history context when there is history", () => {
|
||||||
|
const entries = [
|
||||||
|
{ role: "assistant", entry: { sender: "Assistant", body: "prev" } },
|
||||||
|
{ role: "user", entry: { sender: "User", body: "next" } },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const expected = buildHistoryContextFromEntries({
|
||||||
|
entries: entries.map((e) => e.entry),
|
||||||
|
currentMessage: "User: next",
|
||||||
|
formatEntry: (e) => `${e.sender}: ${e.body}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildAgentMessageFromConversationEntries([...entries])).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers last tool entry over assistant for current message", () => {
|
||||||
|
const entries = [
|
||||||
|
{ role: "user", entry: { sender: "User", body: "question" } },
|
||||||
|
{ role: "tool", entry: { sender: "Tool:x", body: "tool output" } },
|
||||||
|
{ role: "assistant", entry: { sender: "Assistant", body: "assistant text" } },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const expected = buildHistoryContextFromEntries({
|
||||||
|
entries: [entries[0].entry, entries[1].entry],
|
||||||
|
currentMessage: "Tool:x: tool output",
|
||||||
|
formatEntry: (e) => `${e.sender}: ${e.body}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildAgentMessageFromConversationEntries([...entries])).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
|
||||||
|
|
||||||
|
export type ConversationEntry = {
|
||||||
|
role: "user" | "assistant" | "tool";
|
||||||
|
entry: HistoryEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildAgentMessageFromConversationEntries(entries: ConversationEntry[]): string {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the last user/tool entry as "current message" so the agent responds to
|
||||||
|
// the latest user input or tool output, not the assistant's previous message.
|
||||||
|
let currentIndex = -1;
|
||||||
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
||||||
|
const role = entries[i]?.role;
|
||||||
|
if (role === "user" || role === "tool") {
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
currentIndex = entries.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEntry = entries[currentIndex]?.entry;
|
||||||
|
if (!currentEntry) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyEntries = entries.slice(0, currentIndex).map((e) => e.entry);
|
||||||
|
if (historyEntries.length === 0) {
|
||||||
|
return currentEntry.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
|
||||||
|
return buildHistoryContextFromEntries({
|
||||||
|
entries: [...historyEntries, currentEntry],
|
||||||
|
currentMessage: formatEntry(currentEntry),
|
||||||
|
formatEntry,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||||
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
|
|
||||||
import { createDefaultDeps } from "../cli/deps.js";
|
import { createDefaultDeps } from "../cli/deps.js";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
import { agentCommand } from "../commands/agent.js";
|
||||||
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
||||||
import { logWarn } from "../logger.js";
|
import { logWarn } from "../logger.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import {
|
||||||
|
buildAgentMessageFromConversationEntries,
|
||||||
|
type ConversationEntry,
|
||||||
|
} from "./agent-prompt.js";
|
||||||
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
||||||
import {
|
import {
|
||||||
readJsonBodyOrError,
|
readJsonBodyOrError,
|
||||||
@@ -83,8 +86,7 @@ function buildAgentPrompt(messagesUnknown: unknown): {
|
|||||||
const messages = asMessages(messagesUnknown);
|
const messages = asMessages(messagesUnknown);
|
||||||
|
|
||||||
const systemParts: string[] = [];
|
const systemParts: string[] = [];
|
||||||
const conversationEntries: Array<{ role: "user" | "assistant" | "tool"; entry: HistoryEntry }> =
|
const conversationEntries: ConversationEntry[] = [];
|
||||||
[];
|
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg || typeof msg !== "object") {
|
if (!msg || typeof msg !== "object") {
|
||||||
@@ -121,34 +123,7 @@ function buildAgentPrompt(messagesUnknown: unknown): {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = "";
|
const message = buildAgentMessageFromConversationEntries(conversationEntries);
|
||||||
if (conversationEntries.length > 0) {
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = conversationEntries.length - 1; i >= 0; i -= 1) {
|
|
||||||
const entryRole = conversationEntries[i]?.role;
|
|
||||||
if (entryRole === "user" || entryRole === "tool") {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentIndex < 0) {
|
|
||||||
currentIndex = conversationEntries.length - 1;
|
|
||||||
}
|
|
||||||
const currentEntry = conversationEntries[currentIndex]?.entry;
|
|
||||||
if (currentEntry) {
|
|
||||||
const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry);
|
|
||||||
if (historyEntries.length === 0) {
|
|
||||||
message = currentEntry.body;
|
|
||||||
} else {
|
|
||||||
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
|
|
||||||
message = buildHistoryContextFromEntries({
|
|
||||||
entries: [...historyEntries, currentEntry],
|
|
||||||
currentMessage: formatEntry(currentEntry),
|
|
||||||
formatEntry,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/para
|
|||||||
import type { ImageContent } from "../commands/agent/types.js";
|
import type { ImageContent } from "../commands/agent/types.js";
|
||||||
import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js";
|
import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js";
|
||||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||||
import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply/reply/history.js";
|
|
||||||
import { createDefaultDeps } from "../cli/deps.js";
|
import { createDefaultDeps } from "../cli/deps.js";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
import { agentCommand } from "../commands/agent.js";
|
||||||
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
||||||
@@ -36,6 +35,10 @@ import {
|
|||||||
type InputImageSource,
|
type InputImageSource,
|
||||||
} from "../media/input-files.js";
|
} from "../media/input-files.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import {
|
||||||
|
buildAgentMessageFromConversationEntries,
|
||||||
|
type ConversationEntry,
|
||||||
|
} from "./agent-prompt.js";
|
||||||
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
||||||
import {
|
import {
|
||||||
readJsonBodyOrError,
|
readJsonBodyOrError,
|
||||||
@@ -196,8 +199,7 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const systemParts: string[] = [];
|
const systemParts: string[] = [];
|
||||||
const conversationEntries: Array<{ role: "user" | "assistant" | "tool"; entry: HistoryEntry }> =
|
const conversationEntries: ConversationEntry[] = [];
|
||||||
[];
|
|
||||||
|
|
||||||
for (const item of input) {
|
for (const item of input) {
|
||||||
if (item.type === "message") {
|
if (item.type === "message") {
|
||||||
@@ -227,36 +229,7 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
|
|||||||
// Skip reasoning and item_reference for prompt building (Phase 1)
|
// Skip reasoning and item_reference for prompt building (Phase 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = "";
|
const message = buildAgentMessageFromConversationEntries(conversationEntries);
|
||||||
if (conversationEntries.length > 0) {
|
|
||||||
// Find the last user or tool message as the current message
|
|
||||||
let currentIndex = -1;
|
|
||||||
for (let i = conversationEntries.length - 1; i >= 0; i -= 1) {
|
|
||||||
const entryRole = conversationEntries[i]?.role;
|
|
||||||
if (entryRole === "user" || entryRole === "tool") {
|
|
||||||
currentIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentIndex < 0) {
|
|
||||||
currentIndex = conversationEntries.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentEntry = conversationEntries[currentIndex]?.entry;
|
|
||||||
if (currentEntry) {
|
|
||||||
const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry);
|
|
||||||
if (historyEntries.length === 0) {
|
|
||||||
message = currentEntry.body;
|
|
||||||
} else {
|
|
||||||
const formatEntry = (entry: HistoryEntry) => `${entry.sender}: ${entry.body}`;
|
|
||||||
message = buildHistoryContextFromEntries({
|
|
||||||
entries: [...historyEntries, currentEntry],
|
|
||||||
currentMessage: formatEntry(currentEntry),
|
|
||||||
formatEntry,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
|
|||||||
Reference in New Issue
Block a user