mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
💄 style: add the history count limit back in agents params settings (#12199)
* fix: add the history count limit back in agents params settings * fix: fixed the test * fix: change the default settings snap the enableHistoryCount as false * fix: change the history process to the first into MessageEngine * fix: fixed some count limited * fix: fixed the enableHistoryCount check test * fix: change the getEnableHistoryCountById logic
This commit is contained in:
@@ -28,7 +28,7 @@ export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
|
||||
enableAutoCreateTopic: true,
|
||||
enableCompressHistory: true,
|
||||
enableContextCompression: true,
|
||||
enableHistoryCount: true,
|
||||
enableHistoryCount: false,
|
||||
enableReasoning: false,
|
||||
enableStreaming: true,
|
||||
historyCount: 20,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
GroupMessageFlattenProcessor,
|
||||
GroupOrchestrationFilterProcessor,
|
||||
GroupRoleTransformProcessor,
|
||||
HistoryTruncateProcessor,
|
||||
InputTemplateProcessor,
|
||||
MessageCleanupProcessor,
|
||||
MessageContentProcessor,
|
||||
@@ -121,6 +122,8 @@ export class MessagesEngine {
|
||||
provider,
|
||||
systemRole,
|
||||
inputTemplate,
|
||||
enableHistoryCount,
|
||||
historyCount,
|
||||
forceFinish,
|
||||
historySummary,
|
||||
formatHistorySummary,
|
||||
@@ -168,6 +171,17 @@ export class MessagesEngine {
|
||||
const isSystemDateEnabled = enableSystemDate !== false && !hasDateAwareTools;
|
||||
|
||||
return [
|
||||
// =============================================
|
||||
// Phase 0: History Truncation (FIRST - truncate before any processing)
|
||||
// =============================================
|
||||
|
||||
// 0. History truncate (limit message count based on configuration)
|
||||
// This MUST be first to ensure subsequent processors only work with truncated messages
|
||||
new HistoryTruncateProcessor({
|
||||
enableHistoryCount,
|
||||
historyCount,
|
||||
}),
|
||||
|
||||
// =============================================
|
||||
// Phase 1: System Role Injection
|
||||
// =============================================
|
||||
|
||||
@@ -12,8 +12,202 @@ export interface HistoryTruncateConfig {
|
||||
historyCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper interface for message with minimal required fields
|
||||
*/
|
||||
interface MinimalMessage {
|
||||
agentId?: string | 'supervisor';
|
||||
id: string;
|
||||
metadata?: {
|
||||
agentCouncil?: boolean;
|
||||
compare?: boolean;
|
||||
[key: string]: any;
|
||||
} | null;
|
||||
parentId?: string;
|
||||
role: string;
|
||||
tool_call_id?: string;
|
||||
tools?: Array<{ id: string; [key: string]: any }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build helper maps for message relationships
|
||||
*/
|
||||
const buildMessageMaps = (messages: MinimalMessage[]) => {
|
||||
const messageMap = new Map<string, MinimalMessage>();
|
||||
const childrenMap = new Map<string, string[]>();
|
||||
|
||||
// Build messageMap
|
||||
for (const msg of messages) {
|
||||
messageMap.set(msg.id, msg);
|
||||
}
|
||||
|
||||
// Build childrenMap
|
||||
for (const msg of messages) {
|
||||
if (msg.parentId) {
|
||||
const parentChildren = childrenMap.get(msg.parentId) || [];
|
||||
parentChildren.push(msg.id);
|
||||
childrenMap.set(msg.parentId, parentChildren);
|
||||
}
|
||||
}
|
||||
|
||||
return { childrenMap, messageMap };
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect all message IDs in an AssistantGroup chain
|
||||
* Follows the same logic as MessageCollector.collectAssistantChain
|
||||
*/
|
||||
const collectAssistantGroupIds = (
|
||||
assistantId: string,
|
||||
messageMap: Map<string, MinimalMessage>,
|
||||
childrenMap: Map<string, string[]>,
|
||||
collected: Set<string>,
|
||||
): void => {
|
||||
const assistant = messageMap.get(assistantId);
|
||||
if (!assistant || assistant.role !== 'assistant') return;
|
||||
|
||||
// Mark assistant as collected
|
||||
collected.add(assistantId);
|
||||
|
||||
// Get the agentId from the first assistant in chain
|
||||
const groupAgentId = assistant.agentId;
|
||||
|
||||
// Collect tool messages
|
||||
const tools = assistant.tools || [];
|
||||
for (const tool of tools) {
|
||||
const toolId = tool.id;
|
||||
const toolMsg = messageMap.get(toolId);
|
||||
if (!toolMsg) continue;
|
||||
|
||||
// Mark tool as collected
|
||||
collected.add(toolId);
|
||||
|
||||
// Check if tool has agentCouncil metadata (stop here if true)
|
||||
if (toolMsg.metadata?.agentCouncil === true) continue;
|
||||
|
||||
// Check if tool has multiple task children (stop here if true)
|
||||
const toolChildren = childrenMap.get(toolId) || [];
|
||||
const taskChildren = toolChildren.filter((cid) => messageMap.get(cid)?.role === 'task');
|
||||
if (taskChildren.length > 1) continue;
|
||||
|
||||
// Find first child of tool message
|
||||
const firstChildId = toolChildren[0];
|
||||
if (!firstChildId) continue;
|
||||
|
||||
const nextMsg = messageMap.get(firstChildId);
|
||||
if (!nextMsg) continue;
|
||||
|
||||
// If next message is assistant with same agentId, recurse
|
||||
if (nextMsg.role === 'assistant' && nextMsg.agentId === groupAgentId) {
|
||||
collectAssistantGroupIds(firstChildId, messageMap, childrenMap, collected);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a message is the start of a logical group
|
||||
*/
|
||||
const isGroupStart = (
|
||||
msg: MinimalMessage,
|
||||
messageMap: Map<string, MinimalMessage>,
|
||||
childrenMap: Map<string, string[]>,
|
||||
): boolean => {
|
||||
// AssistantGroup: assistant with tools
|
||||
if (msg.role === 'assistant' && msg.tools && msg.tools.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AgentCouncil: tool message with agentCouncil metadata
|
||||
if (msg.role === 'tool' && msg.metadata?.agentCouncil === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare: message with compare metadata
|
||||
if (msg.metadata?.compare === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tasks: first task message in a group of multiple tasks with same parentId
|
||||
if (msg.role === 'task' && msg.parentId) {
|
||||
const siblings = childrenMap.get(msg.parentId) || [];
|
||||
const taskSiblings = siblings.filter((sid) => messageMap.get(sid)?.role === 'task');
|
||||
if (taskSiblings.length > 1 && taskSiblings[0] === msg.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect all message IDs in a logical group starting from a group-start message
|
||||
*/
|
||||
const collectGroupIds = (
|
||||
startId: string,
|
||||
messageMap: Map<string, MinimalMessage>,
|
||||
childrenMap: Map<string, string[]>,
|
||||
): Set<string> => {
|
||||
const collected = new Set<string>();
|
||||
const startMsg = messageMap.get(startId);
|
||||
if (!startMsg) return collected;
|
||||
|
||||
// AssistantGroup
|
||||
if (startMsg.role === 'assistant' && startMsg.tools && startMsg.tools.length > 0) {
|
||||
collectAssistantGroupIds(startId, messageMap, childrenMap, collected);
|
||||
return collected;
|
||||
}
|
||||
|
||||
// AgentCouncil: collect tool + all its children
|
||||
if (startMsg.role === 'tool' && startMsg.metadata?.agentCouncil === true) {
|
||||
collected.add(startId);
|
||||
const children = childrenMap.get(startId) || [];
|
||||
for (const childId of children) {
|
||||
collected.add(childId);
|
||||
// Also collect their tool messages if they have any
|
||||
const child = messageMap.get(childId);
|
||||
if (child?.tools) {
|
||||
for (const tool of child.tools) {
|
||||
collected.add(tool.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
// Compare: collect message + all children
|
||||
if (startMsg.metadata?.compare === true) {
|
||||
collected.add(startId);
|
||||
const children = childrenMap.get(startId) || [];
|
||||
for (const childId of children) {
|
||||
collected.add(childId);
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
// Tasks: collect all task siblings
|
||||
if (startMsg.role === 'task' && startMsg.parentId) {
|
||||
const siblings = childrenMap.get(startMsg.parentId) || [];
|
||||
const taskSiblings = siblings.filter((sid) => messageMap.get(sid)?.role === 'task');
|
||||
if (taskSiblings.length > 1) {
|
||||
// Also include parent tool message
|
||||
collected.add(startMsg.parentId);
|
||||
for (const taskId of taskSiblings) {
|
||||
collected.add(taskId);
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
}
|
||||
|
||||
// Not a group, just the message itself
|
||||
collected.add(startId);
|
||||
return collected;
|
||||
};
|
||||
|
||||
/**
|
||||
* Slice messages based on history count configuration
|
||||
* Uses group-aware counting: messages grouped together in the frontend
|
||||
* (AssistantGroup, AgentCouncil, Compare, Tasks) are counted as a single unit
|
||||
*
|
||||
* @param messages Original messages array
|
||||
* @param options Configuration options for slicing
|
||||
* @returns Sliced messages array
|
||||
@@ -31,8 +225,68 @@ export const getSlicedMessages = (
|
||||
// if historyCount is negative or set to 0, return empty array
|
||||
if (options.historyCount <= 0) return [];
|
||||
|
||||
// if historyCount is positive, return last N messages
|
||||
return messages.slice(-options.historyCount);
|
||||
// Build message relationship maps
|
||||
const { messageMap, childrenMap } = buildMessageMaps(messages as MinimalMessage[]);
|
||||
|
||||
// Step 1: Identify all groups by walking forward
|
||||
// Build messageId → groupIndex mapping
|
||||
const messageToGroup = new Map<string, number>();
|
||||
const groups: Set<string>[] = [];
|
||||
const processed = new Set<string>();
|
||||
let groupIndex = 0;
|
||||
|
||||
// Walk forward to identify all groups
|
||||
for (const msg of messages as MinimalMessage[]) {
|
||||
if (processed.has(msg.id)) continue;
|
||||
|
||||
// Check if this message starts a group
|
||||
if (isGroupStart(msg, messageMap, childrenMap)) {
|
||||
const groupIds = collectGroupIds(msg.id, messageMap, childrenMap);
|
||||
groups.push(groupIds);
|
||||
// Map each message in the group to this group index
|
||||
groupIds.forEach((id) => {
|
||||
messageToGroup.set(id, groupIndex);
|
||||
processed.add(id);
|
||||
});
|
||||
groupIndex++;
|
||||
} else {
|
||||
// Single message (not part of a group)
|
||||
const singleGroup = new Set([msg.id]);
|
||||
groups.push(singleGroup);
|
||||
messageToGroup.set(msg.id, groupIndex);
|
||||
processed.add(msg.id);
|
||||
groupIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Walk backwards through messages to select last N groups
|
||||
const selectedGroupIndices = new Set<number>();
|
||||
for (let i = messages.length - 1; i >= 0 && selectedGroupIndices.size < options.historyCount; i--) {
|
||||
const msg = messages[i] as MinimalMessage;
|
||||
const groupIdx = messageToGroup.get(msg.id);
|
||||
if (groupIdx !== undefined) {
|
||||
selectedGroupIndices.add(groupIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Collect all message IDs from selected groups
|
||||
const selectedIds = new Set<string>();
|
||||
for (const groupIdx of selectedGroupIndices) {
|
||||
const group = groups[groupIdx];
|
||||
if (group) {
|
||||
group.forEach((id) => selectedIds.add(id));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Filter original messages array to keep only selected messages
|
||||
// Preserve original order
|
||||
const result = messages.filter((msg: any) => selectedIds.has(msg.id));
|
||||
|
||||
log(
|
||||
`Group-aware truncation: ${messages.length} messages → ${groups.length} groups → kept ${selectedGroupIndices.size} groups (${result.length} messages)`,
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -54,7 +308,7 @@ export class HistoryTruncateProcessor extends BaseProcessor {
|
||||
|
||||
const originalCount = clonedContext.messages.length;
|
||||
|
||||
// Apply history truncation
|
||||
// Apply group-aware history truncation
|
||||
clonedContext.messages = getSlicedMessages(clonedContext.messages, {
|
||||
enableHistoryCount: this.config.enableHistoryCount,
|
||||
historyCount: this.config.historyCount,
|
||||
@@ -68,7 +322,7 @@ export class HistoryTruncateProcessor extends BaseProcessor {
|
||||
clonedContext.metadata.finalMessageCount = finalCount;
|
||||
|
||||
log(
|
||||
`History truncation completed, truncated ${truncatedCount} messages (${originalCount} → ${finalCount})`,
|
||||
`History truncation completed (group-aware), truncated ${truncatedCount} messages (${originalCount} → ${finalCount})`,
|
||||
);
|
||||
|
||||
return this.markAsExecuted(clonedContext);
|
||||
|
||||
@@ -80,6 +80,181 @@ describe('HistoryTruncateProcessor', () => {
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe('Group-aware truncation', () => {
|
||||
it('should count AssistantGroup as a single unit', () => {
|
||||
// Simulate: user -> assistant with tools -> tool -> assistant (same agentId)
|
||||
const messagesWithGroup = [
|
||||
{ id: '1', content: 'User message 1', role: 'user' },
|
||||
{ id: '2', content: 'User message 2', role: 'user' },
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
content: 'First assistant',
|
||||
id: '3',
|
||||
parentId: '2',
|
||||
role: 'assistant',
|
||||
tools: [{ id: '4' }],
|
||||
},
|
||||
{ id: '4', content: 'Tool result', parentId: '3', role: 'tool', tool_call_id: 'call-1' },
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
content: 'Second assistant',
|
||||
id: '5',
|
||||
parentId: '4',
|
||||
role: 'assistant',
|
||||
},
|
||||
];
|
||||
|
||||
// Groups identified:
|
||||
// - Group 0: user message 1 (id: 1)
|
||||
// - Group 1: user message 2 (id: 2)
|
||||
// - Group 2: AssistantGroup (ids: 3, 4, 5)
|
||||
// historyCount: 2 should keep last 2 groups: Group 1 and Group 2
|
||||
const result = getSlicedMessages(messagesWithGroup, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 2,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result.map((m: any) => m.id)).toEqual(['2', '3', '4', '5']);
|
||||
});
|
||||
|
||||
it('should stop AssistantGroup chain when agentId changes', () => {
|
||||
const messagesWithGroup = [
|
||||
{ id: '1', content: 'User message', role: 'user' },
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
content: 'First assistant',
|
||||
id: '2',
|
||||
parentId: '1',
|
||||
role: 'assistant',
|
||||
tools: [{ id: '3' }],
|
||||
},
|
||||
{ id: '3', content: 'Tool result', parentId: '2', role: 'tool', tool_call_id: 'call-1' },
|
||||
{
|
||||
agentId: 'agent-2', // Different agent!
|
||||
content: 'Second assistant',
|
||||
id: '4',
|
||||
parentId: '3',
|
||||
role: 'assistant',
|
||||
},
|
||||
];
|
||||
|
||||
// historyCount: 1 should only keep the last "group"
|
||||
// Since agent-2 is different, it's a separate single message
|
||||
const result = getSlicedMessages(messagesWithGroup, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 1,
|
||||
});
|
||||
|
||||
// Should keep only the last message (different agentId breaks the group)
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('4');
|
||||
});
|
||||
|
||||
it('should count AgentCouncil as a single unit', () => {
|
||||
const messagesWithCouncil = [
|
||||
{ id: '1', content: 'User message', role: 'user' },
|
||||
{
|
||||
id: '2',
|
||||
content: 'Tool message',
|
||||
metadata: { agentCouncil: true },
|
||||
parentId: '1',
|
||||
role: 'tool',
|
||||
},
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
content: 'Agent 1 response',
|
||||
id: '3',
|
||||
parentId: '2',
|
||||
role: 'assistant',
|
||||
},
|
||||
{
|
||||
agentId: 'agent-2',
|
||||
content: 'Agent 2 response',
|
||||
id: '4',
|
||||
parentId: '2',
|
||||
role: 'assistant',
|
||||
},
|
||||
];
|
||||
|
||||
// historyCount: 1 should keep the entire council (tool + all children)
|
||||
const result = getSlicedMessages(messagesWithCouncil, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 1,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((m: any) => m.id)).toEqual(['2', '3', '4']);
|
||||
});
|
||||
|
||||
it('should count Tasks group as a single unit', () => {
|
||||
const messagesWithTasks = [
|
||||
{ id: '1', content: 'User message', role: 'user' },
|
||||
{ id: '2', content: 'Tool message', parentId: '1', role: 'tool' },
|
||||
{ content: 'Task 1', id: '3', parentId: '2', role: 'task' },
|
||||
{ content: 'Task 2', id: '4', parentId: '2', role: 'task' },
|
||||
{ content: 'Task 3', id: '5', parentId: '2', role: 'task' },
|
||||
];
|
||||
|
||||
// historyCount: 1 should keep the entire task group (tool + all tasks)
|
||||
const result = getSlicedMessages(messagesWithTasks, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 1,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result.map((m: any) => m.id)).toEqual(['2', '3', '4', '5']);
|
||||
});
|
||||
|
||||
it('should count Compare group as a single unit', () => {
|
||||
const messagesWithCompare = [
|
||||
{ id: '1', content: 'User message', role: 'user' },
|
||||
{ content: 'Compare message', id: '2', metadata: { compare: true }, parentId: '1', role: 'user' },
|
||||
{ content: 'Column 1', id: '3', parentId: '2', role: 'assistant' },
|
||||
{ content: 'Column 2', id: '4', parentId: '2', role: 'assistant' },
|
||||
];
|
||||
|
||||
// historyCount: 1 should keep the entire compare group
|
||||
const result = getSlicedMessages(messagesWithCompare, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 1,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((m: any) => m.id)).toEqual(['2', '3', '4']);
|
||||
});
|
||||
|
||||
it('should handle mixed groups correctly', () => {
|
||||
const mixedMessages = [
|
||||
{ id: '1', content: 'User 1', role: 'user' },
|
||||
{ content: 'Assistant 1', id: '2', parentId: '1', role: 'assistant' },
|
||||
{ id: '3', content: 'User 2', role: 'user' },
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
content: 'Assistant with tools',
|
||||
id: '4',
|
||||
parentId: '3',
|
||||
role: 'assistant',
|
||||
tools: [{ id: '5' }],
|
||||
},
|
||||
{ id: '5', parentId: '4', role: 'tool' },
|
||||
{ agentId: 'agent-1', content: 'Final assistant', id: '6', parentId: '5', role: 'assistant' },
|
||||
{ id: '7', content: 'User 3', role: 'user' },
|
||||
];
|
||||
|
||||
// historyCount: 2 should keep:
|
||||
// - Group 1: AssistantGroup (ids 4, 5, 6)
|
||||
// - Group 2: User 3 (id 7)
|
||||
const result = getSlicedMessages(mixedMessages, {
|
||||
enableHistoryCount: true,
|
||||
historyCount: 2,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result.map((m: any) => m.id)).toEqual(['4', '5', '6', '7']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HistoryTruncateProcessor', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TopP,
|
||||
} from '@/features/ModelParamsControl';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentByIdSelectors } from '@/store/agent/selectors';
|
||||
import { agentByIdSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors';
|
||||
import { useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
import { useAgentId } from '../../hooks/useAgentId';
|
||||
@@ -142,7 +142,7 @@ const PARAM_CONFIG = {
|
||||
}
|
||||
>;
|
||||
|
||||
const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
||||
const Controls = memo<ControlsProps>(({ setUpdating, updating }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const mobile = useServerConfigStore((s) => s.isMobile);
|
||||
const agentId = useAgentId();
|
||||
@@ -152,8 +152,17 @@ const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const enableMaxTokens = AntdForm.useWatch(['chatConfig', 'enableMaxTokens'], form);
|
||||
const enableHistoryCount = AntdForm.useWatch(['chatConfig', 'enableHistoryCount'], form);
|
||||
const { frequency_penalty, presence_penalty, temperature, top_p } = config.params ?? {};
|
||||
|
||||
const historyCountFromStore = useAgentStore((s) =>
|
||||
chatConfigByIdSelectors.getHistoryCountById(agentId)(s),
|
||||
);
|
||||
// Use raw chatConfig value, not the selector with business logic that may force false
|
||||
const enableHistoryCountFromStore = useAgentStore(
|
||||
(s) => chatConfigByIdSelectors.getChatConfigById(agentId)(s).enableHistoryCount,
|
||||
);
|
||||
|
||||
const lastValuesRef = useRef<Record<ParamKey, number | undefined>>({
|
||||
frequency_penalty,
|
||||
presence_penalty,
|
||||
@@ -174,6 +183,20 @@ const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
||||
}
|
||||
}, [config, form, frequency_penalty, presence_penalty, temperature, top_p]);
|
||||
|
||||
// Sync history count values to form
|
||||
useEffect(() => {
|
||||
// Skip syncing when updating to avoid overwriting user's in-progress edits
|
||||
if (updating) return;
|
||||
|
||||
form.setFieldsValue({
|
||||
chatConfig: {
|
||||
...form.getFieldValue('chatConfig'),
|
||||
enableHistoryCount: enableHistoryCountFromStore,
|
||||
historyCount: historyCountFromStore,
|
||||
},
|
||||
});
|
||||
}, [form, enableHistoryCountFromStore, historyCountFromStore, updating]);
|
||||
|
||||
const temperatureValue = AntdForm.useWatch(PARAM_NAME_MAP.temperature, form);
|
||||
const topPValue = AntdForm.useWatch(PARAM_NAME_MAP.top_p, form);
|
||||
const presencePenaltyValue = AntdForm.useWatch(PARAM_NAME_MAP.presence_penalty, form);
|
||||
@@ -336,7 +359,55 @@ const Controls = memo<ControlsProps>(({ setUpdating }) => {
|
||||
},
|
||||
];
|
||||
|
||||
const allItems = [...baseItems, ...maxTokensItems, ...contextCompressionItems];
|
||||
// History Count items
|
||||
const historyCountItems: FormItemProps[] = [
|
||||
{
|
||||
children: <Switch />,
|
||||
label: (
|
||||
<Flexbox horizontal align={'center'} className={styles.label} gap={8}>
|
||||
{t('settingChat.enableHistoryCount.title')}
|
||||
<InfoTooltip title={t('settingChat.historyCount.desc')} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['chatConfig', 'enableHistoryCount'],
|
||||
tag: 'history',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
...(enableHistoryCount
|
||||
? [
|
||||
{
|
||||
children: (
|
||||
<SliderWithInput
|
||||
max={20}
|
||||
min={0}
|
||||
step={1}
|
||||
unlimitedInput={true}
|
||||
styles={{
|
||||
input: {
|
||||
maxWidth: 64,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<Flexbox horizontal align={'center'} className={styles.label} gap={8}>
|
||||
{t('settingChat.historyCount.title')}
|
||||
<InfoTooltip title={t('settingChat.historyCount.desc')} />
|
||||
</Flexbox>
|
||||
),
|
||||
name: ['chatConfig', 'historyCount'],
|
||||
tag: 'history',
|
||||
} satisfies FormItemProps,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const allItems = [
|
||||
...baseItems,
|
||||
...maxTokensItems,
|
||||
...contextCompressionItems,
|
||||
...historyCountItems,
|
||||
];
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
agentByIdSelectors,
|
||||
agentChatConfigSelectors,
|
||||
agentSelectors,
|
||||
chatConfigByIdSelectors,
|
||||
} from '@/store/agent/selectors';
|
||||
import { aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
||||
import { getChatStoreState } from '@/store/chat';
|
||||
@@ -241,12 +240,12 @@ class ChatService {
|
||||
const modelMessages = await contextEngineering({
|
||||
agentBuilderContext,
|
||||
agentId: targetAgentId,
|
||||
enableHistoryCount:
|
||||
chatConfigByIdSelectors.getEnableHistoryCountById(targetAgentId)(getAgentStoreState()),
|
||||
// Use raw chatConfig values, not selectors with business logic that may force false
|
||||
enableHistoryCount: chatConfig.enableHistoryCount,
|
||||
enableUserMemories,
|
||||
groupId,
|
||||
historyCount:
|
||||
chatConfigByIdSelectors.getHistoryCountById(targetAgentId)(getAgentStoreState()) + 2,
|
||||
// historyCount + 2 accounts for: 1 user message + 1 assistant response being added
|
||||
historyCount: (chatConfig.historyCount ?? 20) + 2,
|
||||
// Page editor context from agent runtime
|
||||
initialContext: options?.initialContext,
|
||||
inputTemplate: chatConfig.inputTemplate,
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('chatConfigByIdSelectors', () => {
|
||||
});
|
||||
|
||||
describe('getEnableHistoryCountById', () => {
|
||||
it('should return false when context caching enabled and model supports it', () => {
|
||||
it('should return enableHistoryCount value even when context caching is enabled', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
@@ -69,10 +69,10 @@ describe('chatConfigByIdSelectors', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getEnableHistoryCountById('agent-1')(state)).toBe(false);
|
||||
expect(chatConfigByIdSelectors.getEnableHistoryCountById('agent-1')(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when search enabled and model is claude-3-7-sonnet', () => {
|
||||
it('should return enableHistoryCount value even when search is enabled', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
@@ -86,10 +86,10 @@ describe('chatConfigByIdSelectors', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(chatConfigByIdSelectors.getEnableHistoryCountById('agent-1')(state)).toBe(false);
|
||||
expect(chatConfigByIdSelectors.getEnableHistoryCountById('agent-1')(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return enableHistoryCount value when no special cases apply', () => {
|
||||
it('should return enableHistoryCount value directly from config', () => {
|
||||
const state = createState({
|
||||
agentMap: {
|
||||
'agent-1': {
|
||||
@@ -114,7 +114,7 @@ describe('chatConfigByIdSelectors', () => {
|
||||
model: 'gpt-4',
|
||||
},
|
||||
'agent-2': {
|
||||
chatConfig: { disableContextCaching: false, enableHistoryCount: true },
|
||||
chatConfig: { disableContextCaching: false, enableHistoryCount: false },
|
||||
model: 'claude-3-5-sonnet',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_AGENT_SEARCH_FC_MODEL } from '@lobechat/const';
|
||||
import { isContextCachingModel, isThinkingWithToolClaudeModel } from '@lobechat/model-runtime';
|
||||
import { type LobeAgentChatConfig } from '@lobechat/types';
|
||||
|
||||
import { type AgentStoreState } from '@/store/agent/initialState';
|
||||
@@ -16,23 +15,9 @@ const getChatConfigById =
|
||||
(s: AgentStoreState): LobeAgentChatConfig =>
|
||||
agentSelectors.getAgentConfigById(agentId)(s)?.chatConfig || {};
|
||||
|
||||
const getEnableHistoryCountById = (agentId: string) => (s: AgentStoreState) => {
|
||||
const config = agentSelectors.getAgentConfigById(agentId)(s);
|
||||
const chatConfig = getChatConfigById(agentId)(s);
|
||||
|
||||
// If context caching is enabled and the current model type matches, do not enable history count
|
||||
const enableContextCaching = !chatConfig.disableContextCaching;
|
||||
|
||||
if (enableContextCaching && config?.model && isContextCachingModel(config.model)) return false;
|
||||
|
||||
// When search is enabled, do not enable history count for the claude 3.7 sonnet model
|
||||
const searchMode = chatConfig.searchMode || 'auto';
|
||||
const enableSearch = searchMode !== 'off';
|
||||
|
||||
if (enableSearch && config?.model && isThinkingWithToolClaudeModel(config.model)) return false;
|
||||
|
||||
return chatConfig.enableHistoryCount;
|
||||
};
|
||||
// Return raw chatConfig value without business logic overrides
|
||||
const getEnableHistoryCountById = (agentId: string) => (s: AgentStoreState) =>
|
||||
getChatConfigById(agentId)(s).enableHistoryCount;
|
||||
|
||||
const getHistoryCountById =
|
||||
(agentId: string) =>
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('agentChatConfigSelectors', () => {
|
||||
});
|
||||
|
||||
describe('enableHistoryCount', () => {
|
||||
it('should return false when context caching enabled and model supports it', () => {
|
||||
it('should return enableHistoryCount value even when context caching is enabled', () => {
|
||||
const state = createState({
|
||||
activeAgentId: 'agent-1',
|
||||
agentMap: {
|
||||
@@ -154,10 +154,10 @@ describe('agentChatConfigSelectors', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(agentChatConfigSelectors.enableHistoryCount(state)).toBe(false);
|
||||
expect(agentChatConfigSelectors.enableHistoryCount(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when search enabled and model is claude-3-7-sonnet', () => {
|
||||
it('should return enableHistoryCount value even when search is enabled', () => {
|
||||
const state = createState({
|
||||
activeAgentId: 'agent-1',
|
||||
agentMap: {
|
||||
@@ -172,10 +172,10 @@ describe('agentChatConfigSelectors', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(agentChatConfigSelectors.enableHistoryCount(state)).toBe(false);
|
||||
expect(agentChatConfigSelectors.enableHistoryCount(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return enableHistoryCount value when no special cases apply', () => {
|
||||
it('should return enableHistoryCount value directly from config', () => {
|
||||
const state = createState({
|
||||
activeAgentId: 'agent-1',
|
||||
agentMap: {
|
||||
|
||||
@@ -21,8 +21,9 @@ const useModelBuiltinSearch = (s: AgentStoreState) =>
|
||||
const searchFCModel = (s: AgentStoreState) =>
|
||||
chatConfigByIdSelectors.getSearchFCModelById(s.activeAgentId || '')(s);
|
||||
|
||||
// Use raw chatConfig value, not the selector with business logic that may force false
|
||||
const enableHistoryCount = (s: AgentStoreState) =>
|
||||
chatConfigByIdSelectors.getEnableHistoryCountById(s.activeAgentId || '')(s);
|
||||
chatConfigByIdSelectors.getChatConfigById(s.activeAgentId || '')(s).enableHistoryCount;
|
||||
|
||||
const historyCount = (s: AgentStoreState): number =>
|
||||
chatConfigByIdSelectors.getHistoryCountById(s.activeAgentId || '')(s);
|
||||
|
||||
@@ -103,7 +103,7 @@ exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.set
|
||||
"enableAutoCreateTopic": true,
|
||||
"enableCompressHistory": true,
|
||||
"enableContextCompression": true,
|
||||
"enableHistoryCount": true,
|
||||
"enableHistoryCount": false,
|
||||
"enableReasoning": false,
|
||||
"enableStreaming": true,
|
||||
"historyCount": 20,
|
||||
@@ -148,7 +148,7 @@ exports[`settingsSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CON
|
||||
"enableAutoCreateTopic": true,
|
||||
"enableCompressHistory": true,
|
||||
"enableContextCompression": true,
|
||||
"enableHistoryCount": true,
|
||||
"enableHistoryCount": false,
|
||||
"enableReasoning": false,
|
||||
"enableStreaming": true,
|
||||
"historyCount": 20,
|
||||
|
||||
Reference in New Issue
Block a user