mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: update gtd tools( use editor & update metadata ) (#11029)
* feat: use lobehub editor to modify gtd plan * merge origin/dev * feat: show todo in doc portal * feat: use the todoProcess in docs portal * feat: add gtd context engine inject
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
"artifacts.svg.copySuccess": "Image copied successfully",
|
||||
"artifacts.svg.download.png": "Download as PNG",
|
||||
"artifacts.svg.download.svg": "Download as SVG",
|
||||
"document.todos.allCompleted": "All tasks completed",
|
||||
"document.todos.title": "Tasks",
|
||||
"emptyArtifactList": "No Artifacts yet. Use Skills in the conversation, then come back here.",
|
||||
"emptyKnowledgeList": "This list is empty.",
|
||||
"files": "Files",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"artifacts.svg.copySuccess": "图片复制成功",
|
||||
"artifacts.svg.download.png": "下载为 PNG",
|
||||
"artifacts.svg.download.svg": "下载为 SVG",
|
||||
"document.todos.allCompleted": "所有任务已完成",
|
||||
"document.todos.title": "任务",
|
||||
"emptyArtifactList": "当前 Artifacts 列表为空,请在会话中按需使用技能后再查看",
|
||||
"emptyKnowledgeList": "当前知识列表为空",
|
||||
"files": "文件",
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lobechat/types": "workspace:*",
|
||||
"@lobehub/editor": "^1",
|
||||
"@lobehub/ui": "^4.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lobehub/editor": "^1",
|
||||
"@lobehub/ui": "^4",
|
||||
"antd": "^6",
|
||||
"antd-style": "*",
|
||||
|
||||
@@ -1,98 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ReactCodePlugin,
|
||||
ReactCodeblockPlugin,
|
||||
ReactHRPlugin,
|
||||
ReactLinkPlugin,
|
||||
ReactListPlugin,
|
||||
ReactMathPlugin,
|
||||
ReactTablePlugin,
|
||||
} from '@lobehub/editor';
|
||||
import { Editor, useEditor } from '@lobehub/editor/react';
|
||||
import { BuiltinInterventionProps } from '@lobechat/types';
|
||||
import { Flexbox, Input, Text, TextArea } from '@lobehub/ui';
|
||||
import { Flexbox, TextArea } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CreatePlanParams } from '../../types';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
description: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
title: css`
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
`,
|
||||
}));
|
||||
|
||||
const CreatePlanIntervention = memo<BuiltinInterventionProps<CreatePlanParams>>(
|
||||
({ args, onArgsChange, registerBeforeApprove }) => {
|
||||
const { t } = useTranslation('tool');
|
||||
const { styles } = useStyles();
|
||||
|
||||
const [goal, setGoal] = useState(args?.goal || '');
|
||||
const [description, setDescription] = useState(args?.description || '');
|
||||
const [context, setContext] = useState(args?.context || '');
|
||||
|
||||
const editor = useEditor();
|
||||
const editorInitializedRef = useRef(false);
|
||||
|
||||
// Track pending changes
|
||||
const pendingChangesRef = useRef<CreatePlanParams | null>(null);
|
||||
|
||||
// Initialize editor content when args.context changes
|
||||
useEffect(() => {
|
||||
if (editor && args?.context && !editorInitializedRef.current) {
|
||||
editor.setDocument('text', args.context);
|
||||
editorInitializedRef.current = true;
|
||||
}
|
||||
}, [editor, args?.context]);
|
||||
|
||||
// Get current context from editor
|
||||
const getContext = useCallback(() => {
|
||||
if (!editor) return args?.context || '';
|
||||
return (editor.getDocument('text') as unknown as string) || '';
|
||||
}, [editor, args?.context]);
|
||||
|
||||
// Save function
|
||||
const save = useCallback(async () => {
|
||||
if (pendingChangesRef.current) {
|
||||
await onArgsChange?.(pendingChangesRef.current);
|
||||
pendingChangesRef.current = null;
|
||||
}
|
||||
}, [onArgsChange]);
|
||||
const context = getContext();
|
||||
const changes: CreatePlanParams = {
|
||||
context: context || undefined,
|
||||
description,
|
||||
goal,
|
||||
};
|
||||
|
||||
// Always submit current state when approving
|
||||
await onArgsChange?.(changes);
|
||||
pendingChangesRef.current = null;
|
||||
}, [onArgsChange, goal, description, getContext]);
|
||||
|
||||
// Register before approve callback
|
||||
registerBeforeApprove?.('createPlan', save);
|
||||
useEffect(() => {
|
||||
return registerBeforeApprove?.('createPlan', save);
|
||||
}, [registerBeforeApprove, save]);
|
||||
|
||||
const handleGoalChange = useCallback(
|
||||
(value: string) => {
|
||||
setGoal(value);
|
||||
pendingChangesRef.current = {
|
||||
context: context || undefined,
|
||||
context: getContext() || undefined,
|
||||
description,
|
||||
goal: value,
|
||||
};
|
||||
},
|
||||
[context, description],
|
||||
[description, getContext],
|
||||
);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(value: string) => {
|
||||
setDescription(value);
|
||||
pendingChangesRef.current = {
|
||||
context: context || undefined,
|
||||
context: getContext() || undefined,
|
||||
description: value,
|
||||
goal,
|
||||
};
|
||||
},
|
||||
[context, goal],
|
||||
[goal, getContext],
|
||||
);
|
||||
|
||||
const handleContextChange = useCallback(
|
||||
(value: string) => {
|
||||
setContext(value);
|
||||
pendingChangesRef.current = {
|
||||
context: value || undefined,
|
||||
description,
|
||||
goal,
|
||||
};
|
||||
const handleContentChange = useCallback(() => {
|
||||
pendingChangesRef.current = {
|
||||
context: getContext() || undefined,
|
||||
description,
|
||||
goal,
|
||||
};
|
||||
}, [description, goal, getContext]);
|
||||
|
||||
// Focus editor when pressing Enter in description
|
||||
const handleDescriptionKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
editor?.focus();
|
||||
}
|
||||
},
|
||||
[description, goal],
|
||||
[editor],
|
||||
);
|
||||
|
||||
// Focus description when pressing Enter in goal
|
||||
const handleGoalKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// Focus description textarea
|
||||
const descriptionTextarea = document.querySelector(
|
||||
'[data-testid="plan-description"]',
|
||||
) as HTMLTextAreaElement;
|
||||
descriptionTextarea?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox gap={8}>
|
||||
<Text>{t('lobe-gtd.createPlan.goal.label')}</Text>
|
||||
<Input
|
||||
onChange={(e) => handleGoalChange(e.target.value)}
|
||||
placeholder={t('lobe-gtd.createPlan.goal.placeholder')}
|
||||
value={goal}
|
||||
/>
|
||||
</Flexbox>
|
||||
<Flexbox gap={8}>
|
||||
<Text>{t('lobe-gtd.createPlan.description.label')}</Text>
|
||||
<Input
|
||||
onChange={(e) => handleDescriptionChange(e.target.value)}
|
||||
placeholder={t('lobe-gtd.createPlan.description.placeholder')}
|
||||
value={description}
|
||||
/>
|
||||
</Flexbox>
|
||||
<Flexbox gap={8}>
|
||||
<Text>{t('lobe-gtd.createPlan.context.label')}</Text>
|
||||
<TextArea
|
||||
autoSize={{ minRows: 10 }}
|
||||
onChange={(e) => handleContextChange(e.target.value)}
|
||||
<Flexbox
|
||||
gap={8}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
paddingBlock={16}
|
||||
>
|
||||
{/* Goal - Main Title */}
|
||||
<TextArea
|
||||
autoSize={{ minRows: 1 }}
|
||||
className={styles.title}
|
||||
onChange={(e) => handleGoalChange(e.target.value)}
|
||||
onKeyDown={handleGoalKeyDown}
|
||||
placeholder={t('lobe-gtd.createPlan.goal.placeholder')}
|
||||
style={{ padding: 0, resize: 'none' }}
|
||||
value={goal}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
|
||||
{/* Description - Subtitle */}
|
||||
<TextArea
|
||||
autoSize={{ minRows: 1 }}
|
||||
className={styles.description}
|
||||
data-testid="plan-description"
|
||||
onChange={(e) => handleDescriptionChange(e.target.value)}
|
||||
onKeyDown={handleDescriptionKeyDown}
|
||||
placeholder={t('lobe-gtd.createPlan.description.placeholder')}
|
||||
style={{ padding: 0, resize: 'none' }}
|
||||
value={description}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
|
||||
{/* Context - Rich Text Editor */}
|
||||
<div style={{ marginTop: 8, minHeight: 200 }}>
|
||||
<Editor
|
||||
content={args.context}
|
||||
editor={editor}
|
||||
lineEmptyPlaceholder={t('lobe-gtd.createPlan.context.placeholder')}
|
||||
onTextChange={handleContentChange}
|
||||
placeholder={t('lobe-gtd.createPlan.context.placeholder')}
|
||||
value={context}
|
||||
plugins={[
|
||||
ReactListPlugin,
|
||||
ReactCodePlugin,
|
||||
ReactCodeblockPlugin,
|
||||
ReactHRPlugin,
|
||||
ReactLinkPlugin,
|
||||
ReactTablePlugin,
|
||||
ReactMathPlugin,
|
||||
]}
|
||||
style={{
|
||||
minHeight: 200,
|
||||
}}
|
||||
type={'text'}
|
||||
/>
|
||||
</Flexbox>
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -16,11 +16,37 @@ import {
|
||||
type Plan,
|
||||
type RemoveTodosParams,
|
||||
type TodoItem,
|
||||
type TodoState,
|
||||
type UpdatePlanParams,
|
||||
type UpdateTodosParams,
|
||||
} from '../types';
|
||||
import { getTodosFromContext } from './helper';
|
||||
|
||||
/**
|
||||
* Sync todos to the Plan document's metadata
|
||||
* This allows the Plan to track todos persistently
|
||||
*/
|
||||
const syncTodosToPlan = async (topicId: string, todos: TodoState): Promise<void> => {
|
||||
try {
|
||||
// List all documents for this topic with type 'agent/plan'
|
||||
const result = await notebookService.listDocuments({ topicId, type: 'agent/plan' });
|
||||
|
||||
// If there's a plan document, update its metadata with the todos
|
||||
if (result.data.length > 0) {
|
||||
// Update the first (most recent) plan document
|
||||
const planDoc = result.data[0];
|
||||
await notebookService.updateDocument({
|
||||
id: planDoc.id,
|
||||
metadata: { todos },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - todo sync is a non-critical feature
|
||||
console.warn('Failed to sync todos to plan:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// API enum for MVP (Todo + Plan)
|
||||
const GTDApiNameEnum = {
|
||||
clearTodos: GTDApiName.clearTodos,
|
||||
completeTodos: GTDApiName.completeTodos,
|
||||
@@ -77,11 +103,18 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
const addedList = itemsToAdd.map((item) => `- ${item.text}`).join('\n');
|
||||
const actionSummary = `✅ Added ${itemsToAdd.length} item${itemsToAdd.length > 1 ? 's' : ''}:\n${addedList}`;
|
||||
|
||||
const todoState = { items: updatedTodos, updatedAt: now };
|
||||
|
||||
// Sync todos to Plan document if topic exists
|
||||
if (ctx.topicId) {
|
||||
await syncTodosToPlan(ctx.topicId, todoState);
|
||||
}
|
||||
|
||||
return {
|
||||
content: actionSummary + '\n\n' + formatTodoStateSummary(updatedTodos, now),
|
||||
state: {
|
||||
createdItems: itemsToAdd.map((item) => item.text),
|
||||
todos: { items: updatedTodos, updatedAt: now },
|
||||
todos: todoState,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
@@ -154,10 +187,17 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
? `🔄 Applied ${results.length} operation${results.length > 1 ? 's' : ''}:\n${results.map((r) => `- ${r}`).join('\n')}`
|
||||
: 'No operations applied.';
|
||||
|
||||
const todoState = { items: updatedTodos, updatedAt: now };
|
||||
|
||||
// Sync todos to Plan document if topic exists
|
||||
if (ctx.topicId) {
|
||||
await syncTodosToPlan(ctx.topicId, todoState);
|
||||
}
|
||||
|
||||
return {
|
||||
content: actionSummary + '\n\n' + formatTodoStateSummary(updatedTodos, now),
|
||||
state: {
|
||||
todos: { items: updatedTodos, updatedAt: now },
|
||||
todos: todoState,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
@@ -223,11 +263,18 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
actionSummary += `\n\nNote: Ignored invalid indices: ${invalidIndices.join(', ')}`;
|
||||
}
|
||||
|
||||
const todoState = { items: updatedTodos, updatedAt: now };
|
||||
|
||||
// Sync todos to Plan document if topic exists
|
||||
if (ctx.topicId) {
|
||||
await syncTodosToPlan(ctx.topicId, todoState);
|
||||
}
|
||||
|
||||
return {
|
||||
content: actionSummary + '\n\n' + formatTodoStateSummary(updatedTodos, now),
|
||||
state: {
|
||||
completedIndices: validIndices,
|
||||
todos: { items: updatedTodos, updatedAt: now },
|
||||
todos: todoState,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
@@ -287,11 +334,18 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
actionSummary += `\n\nNote: Ignored invalid indices: ${invalidIndices.join(', ')}`;
|
||||
}
|
||||
|
||||
const todoState = { items: updatedTodos, updatedAt: now };
|
||||
|
||||
// Sync todos to Plan document if topic exists
|
||||
if (ctx.topicId) {
|
||||
await syncTodosToPlan(ctx.topicId, todoState);
|
||||
}
|
||||
|
||||
return {
|
||||
content: actionSummary + '\n\n' + formatTodoStateSummary(updatedTodos, now),
|
||||
state: {
|
||||
removedIndices: validIndices,
|
||||
todos: { items: updatedTodos, updatedAt: now },
|
||||
todos: todoState,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
@@ -342,13 +396,19 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const todoState = { items: updatedTodos, updatedAt: now };
|
||||
|
||||
// Sync todos to Plan document if topic exists
|
||||
if (ctx.topicId) {
|
||||
await syncTodosToPlan(ctx.topicId, todoState);
|
||||
}
|
||||
|
||||
return {
|
||||
content: actionSummary + '\n\n' + formatTodoStateSummary(updatedTodos, now),
|
||||
state: {
|
||||
clearedCount,
|
||||
mode,
|
||||
todos: { items: updatedTodos, updatedAt: now },
|
||||
todos: todoState,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
@@ -57,6 +57,9 @@ export interface TodoList {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Alias for TodoList, used for state storage in Plan metadata */
|
||||
export type TodoState = TodoList;
|
||||
|
||||
// ==================== Todo Params ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
AgentBuilderContextInjector,
|
||||
GroupAgentBuilderContextInjector,
|
||||
GroupContextInjector,
|
||||
GTDPlanInjector,
|
||||
GTDTodoInjector,
|
||||
HistorySummaryProvider,
|
||||
KnowledgeInjector,
|
||||
PageEditorContextInjector,
|
||||
@@ -121,6 +123,7 @@ export class MessagesEngine {
|
||||
agentBuilderContext,
|
||||
groupAgentBuilderContext,
|
||||
agentGroup,
|
||||
gtd,
|
||||
userMemory,
|
||||
initialContext,
|
||||
stepContext,
|
||||
@@ -135,6 +138,9 @@ export class MessagesEngine {
|
||||
const isUserMemoryEnabled = userMemory?.enabled && userMemory?.memories;
|
||||
// Page editor is enabled if either direct pageContentContext or initialContext.pageEditor is provided
|
||||
const isPageEditorEnabled = !!pageContentContext || !!initialContext?.pageEditor;
|
||||
// GTD is enabled if gtd.enabled is true and either plan or todos is provided
|
||||
const isGTDPlanEnabled = gtd?.enabled && gtd?.plan;
|
||||
const isGTDTodoEnabled = gtd?.enabled && gtd?.todos;
|
||||
|
||||
return [
|
||||
// =============================================
|
||||
@@ -174,6 +180,11 @@ export class MessagesEngine {
|
||||
// 4. User memory injection (conditionally added, injected first)
|
||||
...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
|
||||
|
||||
// 4.5. GTD Plan injection (conditionally added, after user memory, before knowledge)
|
||||
...(isGTDPlanEnabled
|
||||
? [new GTDPlanInjector({ enabled: true, plan: gtd.plan })]
|
||||
: []),
|
||||
|
||||
// 5. Knowledge injection (full content for agent files + metadata for knowledge bases)
|
||||
new KnowledgeInjector({
|
||||
fileContents: knowledge?.fileContents,
|
||||
@@ -199,13 +210,13 @@ export class MessagesEngine {
|
||||
// 8. Tool system role injection (conditionally added)
|
||||
...(toolsConfig?.manifests && toolsConfig.manifests.length > 0
|
||||
? [
|
||||
new ToolSystemRoleProvider({
|
||||
isCanUseFC: capabilities?.isCanUseFC || (() => true),
|
||||
manifests: toolsConfig.manifests,
|
||||
model,
|
||||
provider,
|
||||
}),
|
||||
]
|
||||
new ToolSystemRoleProvider({
|
||||
isCanUseFC: capabilities?.isCanUseFC || (() => true),
|
||||
manifests: toolsConfig.manifests,
|
||||
model,
|
||||
provider,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
||||
// 9. History summary injection
|
||||
@@ -222,23 +233,28 @@ export class MessagesEngine {
|
||||
? pageContentContext
|
||||
: initialContext?.pageEditor
|
||||
? {
|
||||
markdown: initialContext.pageEditor.markdown,
|
||||
metadata: {
|
||||
charCount: initialContext.pageEditor.metadata.charCount,
|
||||
lineCount: initialContext.pageEditor.metadata.lineCount,
|
||||
title: initialContext.pageEditor.metadata.title,
|
||||
},
|
||||
// Use latest XML from stepContext if available, otherwise fallback to initial XML
|
||||
xml: stepContext?.stepPageEditor?.xml || initialContext.pageEditor.xml,
|
||||
}
|
||||
markdown: initialContext.pageEditor.markdown,
|
||||
metadata: {
|
||||
charCount: initialContext.pageEditor.metadata.charCount,
|
||||
lineCount: initialContext.pageEditor.metadata.lineCount,
|
||||
title: initialContext.pageEditor.metadata.title,
|
||||
},
|
||||
// Use latest XML from stepContext if available, otherwise fallback to initial XML
|
||||
xml: stepContext?.stepPageEditor?.xml || initialContext.pageEditor.xml,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
|
||||
// 10.5. GTD Todo injection (conditionally added, at end of last user message)
|
||||
...(isGTDTodoEnabled
|
||||
? [new GTDTodoInjector({ enabled: true, todos: gtd.todos })]
|
||||
: []),
|
||||
|
||||
// =============================================
|
||||
// Phase 3: Message Transformation
|
||||
// =============================================
|
||||
|
||||
// 10. Input template processing
|
||||
// 11. Input template processing
|
||||
new InputTemplateProcessor({ inputTemplate }),
|
||||
|
||||
// 11. Placeholder variables processing
|
||||
@@ -261,10 +277,10 @@ export class MessagesEngine {
|
||||
// 16. Group message sender identity injection (for multi-agent chat)
|
||||
...(isAgentGroupEnabled
|
||||
? [
|
||||
new GroupMessageSenderProcessor({
|
||||
agentMap: agentGroup.agentMap!,
|
||||
}),
|
||||
]
|
||||
new GroupMessageSenderProcessor({
|
||||
agentMap: agentGroup.agentMap!,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
||||
// =============================================
|
||||
|
||||
@@ -5,6 +5,10 @@ export type {
|
||||
AgentInfo,
|
||||
FileContent,
|
||||
FileContextConfig,
|
||||
GTDConfig,
|
||||
GTDPlan,
|
||||
GTDTodoItem,
|
||||
GTDTodoList,
|
||||
KnowledgeBaseInfo,
|
||||
KnowledgeConfig,
|
||||
MessagesEngineParams,
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { AgentInfo } from '../../processors/GroupMessageSender';
|
||||
import type { AgentBuilderContext } from '../../providers/AgentBuilderContextInjector';
|
||||
import type { GroupAgentBuilderContext } from '../../providers/GroupAgentBuilderContextInjector';
|
||||
import type { GroupMemberInfo } from '../../providers/GroupContextInjector';
|
||||
import type { GTDPlan } from '../../providers/GTDPlanInjector';
|
||||
import type { GTDTodoList } from '../../providers/GTDTodoInjector';
|
||||
import type { LobeToolManifest } from '../tools/types';
|
||||
|
||||
/**
|
||||
@@ -139,6 +141,19 @@ export interface AgentGroupConfig {
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GTD (Getting Things Done) configuration
|
||||
* Used to inject plan and todo context for task management
|
||||
*/
|
||||
export interface GTDConfig {
|
||||
/** Whether GTD context injection is enabled */
|
||||
enabled?: boolean;
|
||||
/** The current plan to inject (injected before first user message) */
|
||||
plan?: GTDPlan;
|
||||
/** The current todo list to inject (injected at end of last user message) */
|
||||
todos?: GTDTodoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* MessagesEngine main parameters
|
||||
*/
|
||||
@@ -190,6 +205,8 @@ export interface MessagesEngineParams {
|
||||
agentGroup?: AgentGroupConfig;
|
||||
/** Group Agent Builder context */
|
||||
groupAgentBuilderContext?: GroupAgentBuilderContext;
|
||||
/** GTD (Getting Things Done) configuration */
|
||||
gtd?: GTDConfig;
|
||||
/** User memory configuration */
|
||||
userMemory?: UserMemoryConfig;
|
||||
|
||||
@@ -235,5 +252,7 @@ export interface MessagesEngineResult {
|
||||
export { type AgentInfo } from '../../processors/GroupMessageSender';
|
||||
export { type AgentBuilderContext } from '../../providers/AgentBuilderContextInjector';
|
||||
export { type GroupAgentBuilderContext } from '../../providers/GroupAgentBuilderContextInjector';
|
||||
export { type GTDPlan } from '../../providers/GTDPlanInjector';
|
||||
export { type GTDTodoItem, type GTDTodoList } from '../../providers/GTDTodoInjector';
|
||||
export { type OpenAIChatMessage, type UIChatMessage } from '@/types/index';
|
||||
export { type FileContent, type KnowledgeBaseInfo } from '@lobechat/prompts';
|
||||
|
||||
106
packages/context-engine/src/providers/GTDPlanInjector.ts
Normal file
106
packages/context-engine/src/providers/GTDPlanInjector.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
|
||||
import type { PipelineContext, ProcessorOptions } from '../types';
|
||||
|
||||
const log = debug('context-engine:provider:GTDPlanInjector');
|
||||
|
||||
/**
|
||||
* GTD Plan data structure
|
||||
* Represents a high-level plan document
|
||||
*/
|
||||
export interface GTDPlan {
|
||||
/** Whether the plan is completed */
|
||||
completed: boolean;
|
||||
/** Detailed context, background, constraints */
|
||||
context?: string;
|
||||
/** Creation timestamp */
|
||||
createdAt: string;
|
||||
/** Brief summary of the plan */
|
||||
description: string;
|
||||
/** The main goal or objective */
|
||||
goal: string;
|
||||
/** Unique plan identifier */
|
||||
id: string;
|
||||
/** Last update timestamp */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface GTDPlanInjectorConfig {
|
||||
/** Whether GTD Plan injection is enabled */
|
||||
enabled?: boolean;
|
||||
/** The current plan to inject */
|
||||
plan?: GTDPlan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format GTD Plan content for injection
|
||||
*/
|
||||
function formatGTDPlan(plan: GTDPlan): string {
|
||||
const lines: string[] = ['<gtd_plan>'];
|
||||
|
||||
lines.push(`<goal>${plan.goal}</goal>`);
|
||||
|
||||
if (plan.description) {
|
||||
lines.push(`<description>${plan.description}</description>`);
|
||||
}
|
||||
|
||||
if (plan.context) {
|
||||
lines.push(`<context>${plan.context}</context>`);
|
||||
}
|
||||
|
||||
lines.push(`<status>${plan.completed ? 'completed' : 'in_progress'}</status>`);
|
||||
lines.push('</gtd_plan>');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* GTD Plan Injector
|
||||
* Responsible for injecting the current plan into context before the first user message
|
||||
* This provides the AI with awareness of the user's current goal and plan context
|
||||
*/
|
||||
export class GTDPlanInjector extends BaseFirstUserContentProvider {
|
||||
readonly name = 'GTDPlanInjector';
|
||||
|
||||
constructor(
|
||||
private config: GTDPlanInjectorConfig,
|
||||
options: ProcessorOptions = {},
|
||||
) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected buildContent(_context: PipelineContext): string | null {
|
||||
const { enabled, plan } = this.config;
|
||||
|
||||
if (!enabled || !plan) {
|
||||
log('GTD Plan not enabled or no plan provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip if plan is completed
|
||||
if (plan.completed) {
|
||||
log('Plan is completed, skipping injection');
|
||||
return null;
|
||||
}
|
||||
|
||||
const formattedContent = formatGTDPlan(plan);
|
||||
|
||||
log(`GTD Plan prepared: goal="${plan.goal}"`);
|
||||
|
||||
return formattedContent;
|
||||
}
|
||||
|
||||
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
||||
const result = await super.doProcess(context);
|
||||
|
||||
// Update metadata
|
||||
if (this.config.enabled && this.config.plan && !this.config.plan.completed) {
|
||||
result.metadata.gtdPlanInjected = true;
|
||||
result.metadata.gtdPlanId = this.config.plan.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
127
packages/context-engine/src/providers/GTDTodoInjector.ts
Normal file
127
packages/context-engine/src/providers/GTDTodoInjector.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { BaseLastUserContentProvider } from '../base/BaseLastUserContentProvider';
|
||||
import type { PipelineContext, ProcessorOptions } from '../types';
|
||||
|
||||
const log = debug('context-engine:provider:GTDTodoInjector');
|
||||
|
||||
/**
|
||||
* GTD Todo item structure
|
||||
*/
|
||||
export interface GTDTodoItem {
|
||||
/** Whether the item is completed */
|
||||
completed: boolean;
|
||||
/** The todo item text */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GTD Todo list structure
|
||||
*/
|
||||
export interface GTDTodoList {
|
||||
items: GTDTodoItem[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface GTDTodoInjectorConfig {
|
||||
/** Whether GTD Todo injection is enabled */
|
||||
enabled?: boolean;
|
||||
/** The current todo list to inject */
|
||||
todos?: GTDTodoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format GTD Todo list content for injection
|
||||
*/
|
||||
function formatGTDTodos(todos: GTDTodoList): string | null {
|
||||
const { items } = todos;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines: string[] = ['<gtd_todos>'];
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const status = item.completed ? 'done' : 'pending';
|
||||
lines.push(`<todo index="${index}" status="${status}">${item.text}</todo>`);
|
||||
});
|
||||
|
||||
const completedCount = items.filter((item) => item.completed).length;
|
||||
const totalCount = items.length;
|
||||
lines.push(`<progress completed="${completedCount}" total="${totalCount}" />`);
|
||||
|
||||
lines.push('</gtd_todos>');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* GTD Todo Injector
|
||||
* Responsible for injecting the current todo list at the end of the last user message
|
||||
* This provides the AI with real-time awareness of task progress
|
||||
*/
|
||||
export class GTDTodoInjector extends BaseLastUserContentProvider {
|
||||
readonly name = 'GTDTodoInjector';
|
||||
|
||||
constructor(
|
||||
private config: GTDTodoInjectorConfig,
|
||||
options: ProcessorOptions = {},
|
||||
) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
||||
log('doProcess called');
|
||||
log('config.enabled:', this.config.enabled);
|
||||
|
||||
const clonedContext = this.cloneContext(context);
|
||||
|
||||
// Skip if GTD Todo is not enabled or no todos
|
||||
if (!this.config.enabled || !this.config.todos) {
|
||||
log('GTD Todo not enabled or no todos, skipping injection');
|
||||
return this.markAsExecuted(clonedContext);
|
||||
}
|
||||
|
||||
// Format todo list content
|
||||
const formattedContent = formatGTDTodos(this.config.todos);
|
||||
|
||||
// Skip if no content to inject (empty todo list)
|
||||
if (!formattedContent) {
|
||||
log('No todos to inject (empty list)');
|
||||
return this.markAsExecuted(clonedContext);
|
||||
}
|
||||
|
||||
log('Formatted content length:', formattedContent.length);
|
||||
|
||||
// Find the last user message index
|
||||
const lastUserIndex = this.findLastUserMessageIndex(clonedContext.messages);
|
||||
|
||||
log('Last user message index:', lastUserIndex);
|
||||
|
||||
if (lastUserIndex === -1) {
|
||||
log('No user messages found, skipping injection');
|
||||
return this.markAsExecuted(clonedContext);
|
||||
}
|
||||
|
||||
// Check if system context wrapper already exists
|
||||
// If yes, only insert context block; if no, use full wrapper
|
||||
const hasExistingWrapper = this.hasExistingSystemContext(clonedContext);
|
||||
const contentToAppend = hasExistingWrapper
|
||||
? this.createContextBlock(formattedContent, 'gtd_todo_context')
|
||||
: this.wrapWithSystemContext(formattedContent, 'gtd_todo_context');
|
||||
|
||||
this.appendToLastUserMessage(clonedContext, contentToAppend);
|
||||
|
||||
// Update metadata
|
||||
clonedContext.metadata.gtdTodoInjected = true;
|
||||
clonedContext.metadata.gtdTodoCount = this.config.todos.items.length;
|
||||
clonedContext.metadata.gtdTodoCompletedCount = this.config.todos.items.filter(
|
||||
(item) => item.completed,
|
||||
).length;
|
||||
|
||||
log('GTD Todo context appended to last user message');
|
||||
|
||||
return this.markAsExecuted(clonedContext);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
export { AgentBuilderContextInjector } from './AgentBuilderContextInjector';
|
||||
export { GroupAgentBuilderContextInjector } from './GroupAgentBuilderContextInjector';
|
||||
export { GroupContextInjector } from './GroupContextInjector';
|
||||
export { GTDPlanInjector } from './GTDPlanInjector';
|
||||
export { GTDTodoInjector } from './GTDTodoInjector';
|
||||
export { HistorySummaryProvider } from './HistorySummary';
|
||||
export { KnowledgeInjector } from './KnowledgeInjector';
|
||||
export { PageEditorContextInjector } from './PageEditorContextInjector';
|
||||
@@ -26,6 +28,8 @@ export type {
|
||||
GroupMemberInfo as GroupContextMemberInfo,
|
||||
} from './GroupContextInjector';
|
||||
export type { HistorySummaryConfig } from './HistorySummary';
|
||||
export type { GTDPlan, GTDPlanInjectorConfig } from './GTDPlanInjector';
|
||||
export type { GTDTodoInjectorConfig, GTDTodoItem, GTDTodoList } from './GTDTodoInjector';
|
||||
export type { KnowledgeInjectorConfig } from './KnowledgeInjector';
|
||||
export type { PageEditorContextInjectorConfig } from './PageEditorContextInjector';
|
||||
export type { SystemRoleInjectorConfig } from './SystemRoleInjector';
|
||||
|
||||
@@ -221,6 +221,10 @@ export interface NotebookDocument {
|
||||
* Document ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Document metadata (e.g., todos for agent/plan documents)
|
||||
*/
|
||||
metadata: Record<string, any> | null;
|
||||
/**
|
||||
* Document title
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import { memo } from 'react';
|
||||
|
||||
import EditorCanvas from './EditorCanvas';
|
||||
import Title from './Title';
|
||||
import TodoList from './TodoList';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
content: css`
|
||||
@@ -13,6 +14,11 @@ const styles = createStaticStyles(({ css }) => ({
|
||||
flex: 1;
|
||||
padding-inline: 12px;
|
||||
`,
|
||||
todoContainer: css`
|
||||
flex-shrink: 0;
|
||||
padding-block-end: 12px;
|
||||
padding-inline: 12px;
|
||||
`,
|
||||
}));
|
||||
|
||||
const DocumentBody = memo(() => {
|
||||
@@ -22,6 +28,9 @@ const DocumentBody = memo(() => {
|
||||
<Title />
|
||||
<EditorCanvas />
|
||||
</div>
|
||||
<div className={styles.todoContainer}>
|
||||
<TodoList />
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
187
src/features/Portal/Document/TodoList.tsx
Normal file
187
src/features/Portal/Document/TodoList.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import { Checkbox, Flexbox, Icon, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { ChevronDown, ChevronUp, ListTodo } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useNotebookStore } from '@/store/notebook';
|
||||
import { notebookSelectors } from '@/store/notebook/selectors';
|
||||
|
||||
import { useDocumentEditorStore } from './store';
|
||||
|
||||
interface TodoItem {
|
||||
completed: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface TodoState {
|
||||
items: TodoItem[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
collapsed: css`
|
||||
max-height: 0;
|
||||
padding-block: 0 !important;
|
||||
opacity: 0;
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: 8px;
|
||||
|
||||
background: ${cssVar.colorBgElevated};
|
||||
|
||||
transition: all 0.2s ${cssVar.motionEaseInOut};
|
||||
|
||||
&:hover {
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
count: css`
|
||||
font-family: ${cssVar.fontFamilyCode};
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
expanded: css`
|
||||
max-height: 300px;
|
||||
opacity: 1;
|
||||
`,
|
||||
header: css`
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
itemRow: css`
|
||||
padding-block: 6px;
|
||||
padding-inline: 4px;
|
||||
border-block-end: 1px dashed ${cssVar.colorBorderSecondary};
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-block-end: none;
|
||||
}
|
||||
`,
|
||||
listContainer: css`
|
||||
overflow: hidden;
|
||||
|
||||
margin-block-start: 8px;
|
||||
padding-block: 4px;
|
||||
border-block-start: 1px solid ${cssVar.colorBorderSecondary};
|
||||
|
||||
transition:
|
||||
max-height 0.25s ${cssVar.motionEaseInOut},
|
||||
opacity 0.2s ${cssVar.motionEaseInOut},
|
||||
padding 0.2s ${cssVar.motionEaseInOut};
|
||||
`,
|
||||
progress: css`
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: ${cssVar.colorFillSecondary};
|
||||
`,
|
||||
progressFill: css`
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: ${cssVar.colorSuccess};
|
||||
transition: width 0.3s ${cssVar.motionEaseInOut};
|
||||
`,
|
||||
textChecked: css`
|
||||
color: ${cssVar.colorTextQuaternary};
|
||||
text-decoration: line-through;
|
||||
`,
|
||||
}));
|
||||
|
||||
const TodoList = memo(() => {
|
||||
const { t } = useTranslation('portal');
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const documentId = useDocumentEditorStore((s) => s.documentId);
|
||||
const topicId = useDocumentEditorStore((s) => s.topicId);
|
||||
|
||||
const document = useNotebookStore(notebookSelectors.getDocumentById(topicId, documentId));
|
||||
|
||||
// Only show for agent/plan documents with todos in metadata
|
||||
if (!document || document.fileType !== 'agent/plan') return null;
|
||||
|
||||
const todos: TodoState | undefined = document.metadata?.todos;
|
||||
const items = todos?.items || [];
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const total = items.length;
|
||||
const completed = items.filter((item) => item.completed).length;
|
||||
const progressPercent = total > 0 ? (completed / total) * 100 : 0;
|
||||
|
||||
// Find current pending task (first incomplete item)
|
||||
const currentPendingTask = items.find((item) => !item.completed);
|
||||
|
||||
const toggleExpanded = () => setExpanded(!expanded);
|
||||
|
||||
return (
|
||||
<div className={styles.container} onClick={toggleExpanded}>
|
||||
{/* Header */}
|
||||
<Flexbox align="center" gap={8} horizontal justify="space-between">
|
||||
<Flexbox align="center" gap={8} horizontal style={{ flex: 1, minWidth: 0 }}>
|
||||
<Icon icon={ListTodo} size={16} style={{ color: cssVar.colorPrimary, flexShrink: 0 }} />
|
||||
<span className={styles.header}>
|
||||
{currentPendingTask?.text || t('document.todos.allCompleted')}
|
||||
</span>
|
||||
<Tag size="small" style={{ flexShrink: 0 }}>
|
||||
<span className={styles.count}>
|
||||
{completed}/{total}
|
||||
</span>
|
||||
</Tag>
|
||||
</Flexbox>
|
||||
<Icon
|
||||
icon={expanded ? ChevronUp : ChevronDown}
|
||||
size={16}
|
||||
style={{ color: cssVar.colorTextTertiary, flexShrink: 0 }}
|
||||
/>
|
||||
</Flexbox>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Flexbox gap={8} horizontal style={{ marginTop: 8 }}>
|
||||
<div className={styles.progress}>
|
||||
<div className={styles.progressFill} style={{ width: `${progressPercent}%` }} />
|
||||
</div>
|
||||
</Flexbox>
|
||||
|
||||
{/* Expandable Todo List */}
|
||||
<div className={cx(styles.listContainer, expanded ? styles.expanded : styles.collapsed)}>
|
||||
{items.map((item, index) => (
|
||||
<Checkbox
|
||||
backgroundColor={cssVar.colorSuccess}
|
||||
checked={item.completed}
|
||||
classNames={{
|
||||
text: item.completed ? styles.textChecked : undefined,
|
||||
wrapper: styles.itemRow,
|
||||
}}
|
||||
key={index}
|
||||
shape="circle"
|
||||
style={{ borderWidth: 1.5, cursor: 'default', pointerEvents: 'none' }}
|
||||
textProps={{
|
||||
type: item.completed ? 'secondary' : undefined,
|
||||
}}
|
||||
>
|
||||
{item.text}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TodoList.displayName = 'TodoList';
|
||||
|
||||
export default TodoList;
|
||||
@@ -10,6 +10,8 @@ export default {
|
||||
'artifacts.svg.copySuccess': 'Image copied successfully',
|
||||
'artifacts.svg.download.png': 'Download as PNG',
|
||||
'artifacts.svg.download.svg': 'Download as SVG',
|
||||
'document.todos.allCompleted': 'All tasks completed',
|
||||
'document.todos.title': 'Tasks',
|
||||
'emptyArtifactList': 'No Artifacts yet. Use Skills in the conversation, then come back here.',
|
||||
'emptyKnowledgeList': 'This list is empty.',
|
||||
'files': 'Files',
|
||||
|
||||
@@ -23,6 +23,7 @@ export const notebookRouter = router({
|
||||
z.object({
|
||||
content: z.string(),
|
||||
description: z.string(),
|
||||
metadata: z.record(z.string(), z.any()).optional(),
|
||||
title: z.string(),
|
||||
topicId: z.string(),
|
||||
type: z
|
||||
@@ -37,6 +38,7 @@ export const notebookRouter = router({
|
||||
content: input.content,
|
||||
description: input.description,
|
||||
fileType: input.type,
|
||||
metadata: input.metadata,
|
||||
source: 'notebook',
|
||||
sourceType: 'api',
|
||||
title: input.title,
|
||||
@@ -90,6 +92,7 @@ export const notebookRouter = router({
|
||||
description: doc.description,
|
||||
fileType: doc.fileType,
|
||||
id: doc.id,
|
||||
metadata: doc.metadata,
|
||||
title: doc.title,
|
||||
totalCharCount: doc.totalCharCount,
|
||||
totalLineCount: doc.totalLineCount,
|
||||
@@ -106,6 +109,7 @@ export const notebookRouter = router({
|
||||
content: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
id: z.string(),
|
||||
metadata: z.record(z.string(), z.any()).optional(),
|
||||
title: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
@@ -127,6 +131,7 @@ export const notebookRouter = router({
|
||||
totalLineCount: contentToUpdate.split('\n').length,
|
||||
}),
|
||||
...(input.description !== undefined && { description: input.description }),
|
||||
...(input.metadata !== undefined && { metadata: input.metadata }),
|
||||
...(input.title && { title: input.title }),
|
||||
});
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ interface GetChatCompletionPayload extends Partial<Omit<ChatStreamPayload, 'mess
|
||||
groupId?: string;
|
||||
messages: UIChatMessage[];
|
||||
scope?: MessageMapScope;
|
||||
topicId?: string;
|
||||
}
|
||||
|
||||
type ChatStreamInputParams = Partial<Omit<ChatStreamPayload, 'messages'>> & {
|
||||
@@ -107,6 +108,7 @@ class ChatService {
|
||||
agentId,
|
||||
groupId,
|
||||
scope,
|
||||
topicId,
|
||||
...params
|
||||
}: GetChatCompletionPayload,
|
||||
options?: FetchOptions,
|
||||
@@ -246,6 +248,7 @@ class ChatService {
|
||||
stepContext: options?.stepContext,
|
||||
systemRole: agentConfig.systemRole,
|
||||
tools: enabledToolIds,
|
||||
topicId,
|
||||
});
|
||||
|
||||
// ============ 3. process extend params ============ //
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AgentBuilderIdentifier } from '@lobechat/builtin-tool-agent-builder';
|
||||
import { GroupAgentBuilderIdentifier } from '@lobechat/builtin-tool-group-agent-builder';
|
||||
import { GTDIdentifier } from '@lobechat/builtin-tool-gtd';
|
||||
import { KLAVIS_SERVER_TYPES, isDesktop } from '@lobechat/const';
|
||||
import {
|
||||
type AgentBuilderContext,
|
||||
type AgentGroupConfig,
|
||||
type GTDConfig,
|
||||
type GroupAgentBuilderContext,
|
||||
type GroupOfficialToolItem,
|
||||
type LobeToolManifest,
|
||||
@@ -20,6 +22,7 @@ import { VARIABLE_GENERATORS } from '@lobechat/utils/client';
|
||||
import debug from 'debug';
|
||||
|
||||
import { isCanUseFC } from '@/helpers/isCanUseFC';
|
||||
import { notebookService } from '@/services/notebook';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { getChatGroupStoreState } from '@/store/agentGroup';
|
||||
@@ -67,6 +70,8 @@ interface ContextEngineeringContext {
|
||||
stepContext?: RuntimeStepContext;
|
||||
systemRole?: string;
|
||||
tools?: string[];
|
||||
/** Topic ID for GTD context injection */
|
||||
topicId?: string;
|
||||
}
|
||||
|
||||
// REVIEW:可能这里可以约束一下 identity,preference,exp 的 重新排序或者裁切过的上下文进来而不是全部丢进来
|
||||
@@ -87,6 +92,7 @@ export const contextEngineering = async ({
|
||||
groupId,
|
||||
initialContext,
|
||||
stepContext,
|
||||
topicId,
|
||||
}: ContextEngineeringContext): Promise<OpenAIChatMessage[]> => {
|
||||
log('tools: %o', tools);
|
||||
|
||||
@@ -255,6 +261,50 @@ export const contextEngineering = async ({
|
||||
userMemoryData = combineUserMemoryData(topicMemories, globalIdentities);
|
||||
}
|
||||
|
||||
// Resolve GTD context: plan and todos
|
||||
// GTD tool must be enabled and topicId must be provided
|
||||
const isGTDEnabled = tools?.includes(GTDIdentifier) ?? false;
|
||||
let gtdConfig: GTDConfig | undefined;
|
||||
|
||||
if (isGTDEnabled && topicId) {
|
||||
try {
|
||||
// Fetch plan document for the current topic
|
||||
const planResult = await notebookService.listDocuments({
|
||||
topicId,
|
||||
type: 'agent/plan',
|
||||
});
|
||||
|
||||
if (planResult.data.length > 0) {
|
||||
const planDoc = planResult.data[0]; // Most recent plan
|
||||
|
||||
// Build plan object for injection
|
||||
const plan = {
|
||||
completed: false, // TODO: Add completed field to document if needed
|
||||
context: planDoc.content ?? undefined,
|
||||
createdAt: planDoc.createdAt.toISOString(),
|
||||
description: planDoc.description || '',
|
||||
goal: planDoc.title || '',
|
||||
id: planDoc.id,
|
||||
updatedAt: planDoc.updatedAt.toISOString(),
|
||||
};
|
||||
|
||||
// Get todos from plan's metadata
|
||||
const todos = planDoc.metadata?.todos;
|
||||
|
||||
gtdConfig = {
|
||||
enabled: true,
|
||||
plan,
|
||||
todos,
|
||||
};
|
||||
|
||||
log('GTD context resolved: plan=%s, todos=%o', plan.goal, todos?.items?.length ?? 0);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - GTD context is optional
|
||||
log('Failed to resolve GTD context:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create MessagesEngine with injected dependencies
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
const engine = new MessagesEngine({
|
||||
@@ -315,6 +365,7 @@ export const contextEngineering = async ({
|
||||
...(isAgentBuilderEnabled && { agentBuilderContext }),
|
||||
...(isGroupAgentBuilderEnabled && { groupAgentBuilderContext }),
|
||||
...(agentGroup && { agentGroup }),
|
||||
...(gtdConfig && { gtd: gtdConfig }),
|
||||
});
|
||||
|
||||
const result = await engine.process();
|
||||
|
||||
@@ -7,6 +7,7 @@ type ExtendedDocumentType = DocumentType | 'agent/plan';
|
||||
interface CreateDocumentParams {
|
||||
content: string;
|
||||
description: string;
|
||||
metadata?: Record<string, any>;
|
||||
title: string;
|
||||
topicId: string;
|
||||
type?: ExtendedDocumentType;
|
||||
@@ -17,6 +18,7 @@ interface UpdateDocumentParams {
|
||||
content?: string;
|
||||
description?: string;
|
||||
id: string;
|
||||
metadata?: Record<string, any>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +404,7 @@ export const streamingExecutor: StateCreator<
|
||||
model,
|
||||
provider,
|
||||
scope, // Pass scope to chat service for page-agent injection
|
||||
topicId, // Pass topicId for GTD context injection
|
||||
...finalAgentConfig.params,
|
||||
plugins: finalAgentConfig.plugins,
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ type ExtendedDocumentType = DocumentType | 'agent/plan';
|
||||
interface CreateDocumentParams {
|
||||
content: string;
|
||||
description: string;
|
||||
metadata?: Record<string, any>;
|
||||
title: string;
|
||||
topicId: string;
|
||||
type?: ExtendedDocumentType;
|
||||
@@ -30,6 +31,7 @@ interface UpdateDocumentParams {
|
||||
content?: string;
|
||||
description?: string;
|
||||
id: string;
|
||||
metadata?: Record<string, any>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user