mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: fix group sub task execution (#11595)
* update memory manifest * support console error in the server with subagent task * ✅ test(agent-service): add unit tests for getAgentConfig method Add 7 test cases for the new getAgentConfig(idOrSlug) method: - Return null if agent does not exist - Support lookup by agent id - Support lookup by slug - Merge DEFAULT_AGENT_CONFIG and serverDefaultAgentConfig - Use default model/provider when agent has none - Prioritize agent model/provider over defaults - Merge user default agent config Relates to: LOBE-3514 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✨ feat(group-agent-builder): add GetAgentInfo Inspector Add Inspector component for getAgentInfo API to display agent avatar and name in the tool call UI. Changes: - Add GetAgentInfoInspector component with avatar and title display - Register inspector in GroupAgentBuilderInspectors registry - Add i18n translations for en-US and zh-CN 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix lobehub manifest temporarily * fix twitter calling * 🔧 chore: remove unused serializeError function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 🐛 fix(test): fix execAgent.threadId test mock for AgentService Add AgentService mock and use importOriginal for model-bank mock to fix test failures after refactoring to use AgentService. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
"builtins.lobe-cloud-sandbox.title": "Cloud Sandbox",
|
||||
"builtins.lobe-group-agent-builder.apiName.batchCreateAgents": "Batch create agents",
|
||||
"builtins.lobe-group-agent-builder.apiName.createAgent": "Create agent",
|
||||
"builtins.lobe-group-agent-builder.apiName.getAgentInfo": "Get member info",
|
||||
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "Get available models",
|
||||
"builtins.lobe-group-agent-builder.apiName.installPlugin": "Install Skill",
|
||||
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "Invite member",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"builtins.lobe-cloud-sandbox.title": "云端沙盒",
|
||||
"builtins.lobe-group-agent-builder.apiName.batchCreateAgents": "批量创建 Agent",
|
||||
"builtins.lobe-group-agent-builder.apiName.createAgent": "创建助理",
|
||||
"builtins.lobe-group-agent-builder.apiName.getAgentInfo": "获取成员信息",
|
||||
"builtins.lobe-group-agent-builder.apiName.getAvailableModels": "获取可用模型",
|
||||
"builtins.lobe-group-agent-builder.apiName.installPlugin": "安装技能",
|
||||
"builtins.lobe-group-agent-builder.apiName.inviteAgent": "邀请成员",
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface AgentState {
|
||||
tools?: any[];
|
||||
systemRole?: string;
|
||||
toolManifestMap: Record<string, any>;
|
||||
/** Tool source map for routing tool execution to correct handler */
|
||||
toolSourceMap?: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'>;
|
||||
|
||||
/**
|
||||
* Model runtime configuration
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import type { BuiltinInspectorProps } from '@lobechat/types';
|
||||
import { Avatar, Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { shinyTextStyles } from '@/styles';
|
||||
|
||||
import type { GetAgentInfoParams } from '../../../types';
|
||||
|
||||
interface GetAgentInfoState {
|
||||
avatar?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar: cv }) => ({
|
||||
root: css`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
`,
|
||||
title: css`
|
||||
flex-shrink: 0;
|
||||
color: ${cv.colorTextSecondary};
|
||||
white-space: nowrap;
|
||||
`,
|
||||
}));
|
||||
|
||||
export const GetAgentInfoInspector = memo<
|
||||
BuiltinInspectorProps<GetAgentInfoParams, GetAgentInfoState>
|
||||
>(({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const agentId = args?.agentId || partialArgs?.agentId;
|
||||
const title = pluginState?.title;
|
||||
const avatar = pluginState?.avatar;
|
||||
|
||||
// Initial streaming state
|
||||
if (isArgumentsStreaming && !agentId) {
|
||||
return (
|
||||
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
|
||||
<span>{t('builtins.lobe-group-agent-builder.apiName.getAgentInfo')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.root, (isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText)}
|
||||
gap={8}
|
||||
horizontal
|
||||
>
|
||||
<span className={styles.title}>
|
||||
{t('builtins.lobe-group-agent-builder.apiName.getAgentInfo')}:
|
||||
</span>
|
||||
{avatar && <Avatar avatar={avatar} shape={'square'} size={20} title={title || undefined} />}
|
||||
<span>{title || agentId}</span>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
GetAgentInfoInspector.displayName = 'GetAgentInfoInspector';
|
||||
|
||||
export default GetAgentInfoInspector;
|
||||
@@ -10,6 +10,7 @@ import { type BuiltinInspector } from '@lobechat/types';
|
||||
import { GroupAgentBuilderApiName } from '../../types';
|
||||
import { BatchCreateAgentsInspector } from './BatchCreateAgents';
|
||||
import { CreateAgentInspector } from './CreateAgent';
|
||||
import { GetAgentInfoInspector } from './GetAgentInfo';
|
||||
import { InviteAgentInspector } from './InviteAgent';
|
||||
import { RemoveAgentInspector } from './RemoveAgent';
|
||||
import { SearchAgentInspector } from './SearchAgent';
|
||||
@@ -27,6 +28,7 @@ export const GroupAgentBuilderInspectors: Record<string, BuiltinInspector> = {
|
||||
// Group-specific inspectors
|
||||
[GroupAgentBuilderApiName.batchCreateAgents]: BatchCreateAgentsInspector as BuiltinInspector,
|
||||
[GroupAgentBuilderApiName.createAgent]: CreateAgentInspector as BuiltinInspector,
|
||||
[GroupAgentBuilderApiName.getAgentInfo]: GetAgentInfoInspector as BuiltinInspector,
|
||||
[GroupAgentBuilderApiName.inviteAgent]: InviteAgentInspector as BuiltinInspector,
|
||||
[GroupAgentBuilderApiName.removeAgent]: RemoveAgentInspector as BuiltinInspector,
|
||||
[GroupAgentBuilderApiName.searchAgent]: SearchAgentInspector as BuiltinInspector,
|
||||
@@ -44,6 +46,7 @@ export const GroupAgentBuilderInspectors: Record<string, BuiltinInspector> = {
|
||||
// Re-export individual inspectors
|
||||
export { BatchCreateAgentsInspector } from './BatchCreateAgents';
|
||||
export { CreateAgentInspector } from './CreateAgent';
|
||||
export { GetAgentInfoInspector } from './GetAgentInfo';
|
||||
export { InviteAgentInspector } from './InviteAgent';
|
||||
export { RemoveAgentInspector } from './RemoveAgent';
|
||||
export { SearchAgentInspector } from './SearchAgent';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
addIdentityJsonSchema,
|
||||
contextMemoryJsonSchema,
|
||||
experienceMemoryJsonSchema,
|
||||
preferenceMemoryJsonSchema,
|
||||
removeIdentityJsonSchema,
|
||||
searchMemoryJsonSchema,
|
||||
updateIdentityJsonSchema,
|
||||
} from '@lobechat/memory-user-memory/schemas';
|
||||
import type { BuiltinToolManifest } from '@lobechat/types';
|
||||
import {
|
||||
CONTEXT_OBJECT_TYPES,
|
||||
CONTEXT_STATUS,
|
||||
CONTEXT_SUBJECT_TYPES,
|
||||
IDENTITY_TYPES,
|
||||
MEMORY_TYPES,
|
||||
MERGE_STRATEGIES,
|
||||
RELATIONSHIPS,
|
||||
} from '@lobechat/types';
|
||||
|
||||
import { systemPrompt } from './systemRole';
|
||||
import { MemoryApiName } from './types';
|
||||
@@ -20,43 +20,608 @@ export const MemoryManifest: BuiltinToolManifest = {
|
||||
description:
|
||||
'Retrieve memories based on a search query. Use this to recall previously saved information.',
|
||||
name: MemoryApiName.searchUserMemory,
|
||||
parameters: searchMemoryJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
topK: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
contexts: { minimum: 0, type: 'integer' },
|
||||
experiences: { minimum: 0, type: 'integer' },
|
||||
preferences: { minimum: 0, type: 'integer' },
|
||||
},
|
||||
required: ['contexts', 'experiences', 'preferences'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: ['query', 'topK'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Create a context memory that captures ongoing situations, projects, or environments. Include actors, resources, statuses, urgency/impact, and a clear description.',
|
||||
name: MemoryApiName.addContextMemory,
|
||||
parameters: contextMemoryJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
details: {
|
||||
description: 'Optional detailed information',
|
||||
type: 'string',
|
||||
},
|
||||
memoryCategory: {
|
||||
description: 'Memory category',
|
||||
type: 'string',
|
||||
},
|
||||
memoryType: {
|
||||
description: 'Memory type',
|
||||
enum: MEMORY_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
summary: {
|
||||
description: 'Concise overview of this specific memory',
|
||||
type: 'string',
|
||||
},
|
||||
tags: {
|
||||
description: 'User defined tags that summarize the context facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
title: {
|
||||
description: 'Brief descriptive title',
|
||||
type: 'string',
|
||||
},
|
||||
withContext: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
associatedObjects: {
|
||||
description:
|
||||
'Array of objects describing involved roles, entities, or resources, [] empty if none',
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
extra: {
|
||||
description:
|
||||
'Additional metadata about the object, should always be a valid JSON string if present',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
name: {
|
||||
description: 'Name of the associated object',
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
description: 'Type/category of the associated object',
|
||||
enum: CONTEXT_OBJECT_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['extra', 'name', 'type'],
|
||||
type: 'object',
|
||||
},
|
||||
type: 'array',
|
||||
},
|
||||
associatedSubjects: {
|
||||
description:
|
||||
'Array of JSON objects describing involved subjects or participants, [] empty if none',
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
extra: {
|
||||
description:
|
||||
'Additional metadata about the subject, should always be a valid JSON string if present',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
name: {
|
||||
description: 'Name of the associated subject',
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
description: 'Type/category of the associated subject',
|
||||
enum: CONTEXT_SUBJECT_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['extra', 'name', 'type'],
|
||||
type: 'object',
|
||||
},
|
||||
type: 'array',
|
||||
},
|
||||
currentStatus: {
|
||||
description:
|
||||
"High level status markers (must be one of 'planned', 'ongoing', 'completed', 'aborted', 'on_hold', 'cancelled')",
|
||||
enum: CONTEXT_STATUS,
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
description: 'Rich narrative describing the situation, timeline, or environment',
|
||||
type: 'string',
|
||||
},
|
||||
labels: {
|
||||
description: 'Model generated tags that summarize the context themes',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
scoreImpact: {
|
||||
description: 'Numeric score (0-1 (0% to 100%)) describing importance',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
scoreUrgency: {
|
||||
description: 'Numeric score (0-1 (0% to 100%)) describing urgency',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
title: {
|
||||
description: 'Optional synthesized context headline',
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
description:
|
||||
"High level context archetype (e.g., 'project', 'relationship', 'goal')",
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'associatedObjects',
|
||||
'associatedSubjects',
|
||||
'currentStatus',
|
||||
'description',
|
||||
'labels',
|
||||
'scoreImpact',
|
||||
'scoreUrgency',
|
||||
'title',
|
||||
'type',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'details',
|
||||
'memoryCategory',
|
||||
'memoryType',
|
||||
'summary',
|
||||
'tags',
|
||||
'title',
|
||||
'withContext',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Record an experience memory capturing situation, actions, reasoning, outcomes, and confidence. Use for lessons, playbooks, or transferable know-how.',
|
||||
name: MemoryApiName.addExperienceMemory,
|
||||
parameters: experienceMemoryJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
details: {
|
||||
description: 'Optional detailed information',
|
||||
type: 'string',
|
||||
},
|
||||
memoryCategory: {
|
||||
description: 'Memory category',
|
||||
type: 'string',
|
||||
},
|
||||
memoryType: {
|
||||
description: 'Memory type',
|
||||
enum: MEMORY_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
summary: {
|
||||
description: 'Concise overview of this specific memory',
|
||||
type: 'string',
|
||||
},
|
||||
tags: {
|
||||
description: 'Model generated tags that summarize the experience facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
title: {
|
||||
description: 'Brief descriptive title',
|
||||
type: 'string',
|
||||
},
|
||||
withExperience: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
action: {
|
||||
description: 'Narrative describing actions taken or behaviors exhibited',
|
||||
type: 'string',
|
||||
},
|
||||
keyLearning: {
|
||||
description: 'Narrative describing key insights or lessons learned',
|
||||
type: 'string',
|
||||
},
|
||||
knowledgeValueScore: {
|
||||
description:
|
||||
'Numeric score (0-1) describing how reusable and shareable this experience is',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
labels: {
|
||||
description: 'Model generated tags that summarize the experience facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
possibleOutcome: {
|
||||
description: 'Narrative describing potential outcomes or learnings',
|
||||
type: 'string',
|
||||
},
|
||||
problemSolvingScore: {
|
||||
description:
|
||||
'Numeric score (0-1) describing how effectively the problem was solved',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
reasoning: {
|
||||
description: 'Narrative describing the thought process or motivations',
|
||||
type: 'string',
|
||||
},
|
||||
scoreConfidence: {
|
||||
description:
|
||||
'Numeric score (0-1 (0% to 100%)) describing confidence in the experience details',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
situation: {
|
||||
description: 'Narrative describing the situation or event',
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
description: 'Type of experience being recorded',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'action',
|
||||
'keyLearning',
|
||||
'knowledgeValueScore',
|
||||
'labels',
|
||||
'possibleOutcome',
|
||||
'problemSolvingScore',
|
||||
'reasoning',
|
||||
'scoreConfidence',
|
||||
'situation',
|
||||
'type',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'details',
|
||||
'memoryCategory',
|
||||
'memoryType',
|
||||
'summary',
|
||||
'tags',
|
||||
'title',
|
||||
'withExperience',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Add an identity memory describing enduring facts about a person, their role, relationship, and supporting evidence. Use to track self/others identities.',
|
||||
name: MemoryApiName.addIdentityMemory,
|
||||
parameters: addIdentityJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
details: {
|
||||
description: 'Optional detailed information',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
memoryCategory: {
|
||||
description: 'Memory category',
|
||||
type: 'string',
|
||||
},
|
||||
memoryType: {
|
||||
description: 'Memory type',
|
||||
enum: MEMORY_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
summary: {
|
||||
description: 'Concise overview of this specific memory',
|
||||
type: 'string',
|
||||
},
|
||||
tags: {
|
||||
description: 'Model generated tags that summarize the identity facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
title: {
|
||||
description:
|
||||
'Honorific-style, concise descriptor (strength + domain/milestone), avoid bare job titles; e.g., "Trusted open-source maintainer", "Specializes in low-latency infra", "Former Aliyun engineer", "Cares for rescue cats"',
|
||||
type: 'string',
|
||||
},
|
||||
withIdentity: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
description: { type: 'string' },
|
||||
episodicDate: { type: ['string', 'null'] },
|
||||
extractedLabels: {
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
relationship: {
|
||||
enum: RELATIONSHIPS,
|
||||
type: 'string',
|
||||
},
|
||||
role: {
|
||||
description:
|
||||
'Role explicitly mentioned for this identity entry (e.g., "platform engineer", "caregiver"); keep neutral and only use when evidence exists',
|
||||
type: 'string',
|
||||
},
|
||||
scoreConfidence: { type: 'number' },
|
||||
sourceEvidence: { type: ['string', 'null'] },
|
||||
type: {
|
||||
enum: IDENTITY_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'description',
|
||||
'episodicDate',
|
||||
'extractedLabels',
|
||||
'relationship',
|
||||
'role',
|
||||
'scoreConfidence',
|
||||
'sourceEvidence',
|
||||
'type',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'details',
|
||||
'memoryCategory',
|
||||
'memoryType',
|
||||
'summary',
|
||||
'tags',
|
||||
'title',
|
||||
'withIdentity',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Create a preference memory that encodes durable directives or choices the assistant should follow. Include conclusionDirectives, scopes, and context.',
|
||||
name: MemoryApiName.addPreferenceMemory,
|
||||
parameters: preferenceMemoryJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
details: {
|
||||
description: 'Optional detailed information',
|
||||
type: 'string',
|
||||
},
|
||||
memoryCategory: {
|
||||
description: 'Memory category',
|
||||
type: 'string',
|
||||
},
|
||||
memoryType: {
|
||||
description: 'Memory type',
|
||||
enum: MEMORY_TYPES,
|
||||
type: 'string',
|
||||
},
|
||||
summary: {
|
||||
description: 'Concise overview of this specific memory',
|
||||
type: 'string',
|
||||
},
|
||||
tags: {
|
||||
description: 'Model generated tags that summarize the preference facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
title: {
|
||||
description: 'Brief descriptive title',
|
||||
type: 'string',
|
||||
},
|
||||
withPreference: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
appContext: {
|
||||
additionalProperties: false,
|
||||
description: 'Application/surface specific preference, if any',
|
||||
properties: {
|
||||
app: {
|
||||
description: 'App or product name this applies to',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
feature: { type: ['string', 'null'] },
|
||||
route: { type: ['string', 'null'] },
|
||||
surface: {
|
||||
description: 'e.g., chat, emails, code review, notes',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
},
|
||||
required: ['app', 'feature', 'route', 'surface'],
|
||||
type: ['object', 'null'],
|
||||
},
|
||||
conclusionDirectives: {
|
||||
description:
|
||||
"Direct, self-contained instruction to the assistant from the user's perspective (what to do, not how to implement)",
|
||||
type: 'string',
|
||||
},
|
||||
extractedLabels: {
|
||||
description: 'Model generated tags that summarize the preference facets',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
extractedScopes: {
|
||||
description:
|
||||
'Array of JSON strings describing preference facets and applicable scopes',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
originContext: {
|
||||
additionalProperties: false,
|
||||
description: 'Context of how/why this preference was expressed',
|
||||
properties: {
|
||||
actor: {
|
||||
description: "Who stated the preference; use 'User' for the user",
|
||||
type: 'string',
|
||||
},
|
||||
applicableWhen: {
|
||||
description: 'Conditions where this preference applies',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
notApplicableWhen: {
|
||||
description: 'Conditions where it does not apply',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
scenario: {
|
||||
description: 'Applicable scenario or use case',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
trigger: {
|
||||
description: 'What prompted this preference',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
},
|
||||
required: ['actor', 'applicableWhen', 'notApplicableWhen', 'scenario', 'trigger'],
|
||||
type: ['object', 'null'],
|
||||
},
|
||||
scorePriority: {
|
||||
description:
|
||||
'Numeric prioritization weight (0-1 (0% to 100%)) where higher means more critical to respect',
|
||||
maximum: 1,
|
||||
minimum: 0,
|
||||
type: 'number',
|
||||
},
|
||||
suggestions: {
|
||||
description: 'Follow-up actions or assistant guidance derived from the preference',
|
||||
items: { type: 'string' },
|
||||
type: 'array',
|
||||
},
|
||||
type: {
|
||||
description:
|
||||
"High level preference classification (e.g., 'lifestyle', 'communication')",
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'appContext',
|
||||
'conclusionDirectives',
|
||||
'extractedLabels',
|
||||
'extractedScopes',
|
||||
'originContext',
|
||||
'scorePriority',
|
||||
'suggestions',
|
||||
'type',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'title',
|
||||
'summary',
|
||||
'tags',
|
||||
'details',
|
||||
'memoryCategory',
|
||||
'memoryType',
|
||||
'withPreference',
|
||||
],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Update an existing identity memory with refined details, relationships, roles, or tags. Use mergeStrategy to control replacement vs merge.',
|
||||
name: MemoryApiName.updateIdentityMemory,
|
||||
parameters: updateIdentityJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
mergeStrategy: {
|
||||
enum: MERGE_STRATEGIES,
|
||||
type: 'string',
|
||||
},
|
||||
set: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
details: {
|
||||
description: 'Optional detailed information, use null for omitting the field',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
memoryCategory: {
|
||||
description: 'Memory category, use null for omitting the field',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
memoryType: {
|
||||
description: 'Memory type, use null for omitting the field',
|
||||
enum: [...MEMORY_TYPES, null],
|
||||
},
|
||||
summary: {
|
||||
description:
|
||||
'Concise overview of this specific memory, use null for omitting the field',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
tags: {
|
||||
description:
|
||||
'Model generated tags that summarize the identity facets, use null for omitting the field',
|
||||
items: { type: 'string' },
|
||||
type: ['array', 'null'],
|
||||
},
|
||||
title: {
|
||||
description:
|
||||
'Honorific-style, concise descriptor (strength + domain/milestone), avoid bare job titles; e.g., "Trusted open-source maintainer", "Specializes in low-latency infra", "Former Aliyun engineer", "Cares for rescue cats"; use null for omitting the field',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
withIdentity: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
description: { type: ['string', 'null'] },
|
||||
episodicDate: { type: ['string', 'null'] },
|
||||
extractedLabels: {
|
||||
items: { type: 'string' },
|
||||
type: ['array', 'null'],
|
||||
},
|
||||
relationship: {
|
||||
description: `Possible values: ${RELATIONSHIPS.join(' | ')}`,
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
role: {
|
||||
description:
|
||||
'Role explicitly mentioned for this identity entry (e.g., "platform engineer", "caregiver"); keep existing when not updated; use null for omitting the field',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
scoreConfidence: { type: ['number', 'null'] },
|
||||
sourceEvidence: { type: ['string', 'null'] },
|
||||
type: {
|
||||
description: `Possible values: ${IDENTITY_TYPES.join(' | ')}`,
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
},
|
||||
required: ['description', 'extractedLabels', 'role'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: ['withIdentity'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: ['id', 'mergeStrategy', 'set'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Remove an identity memory when it is incorrect, obsolete, or duplicated. Always provide a concise reason.',
|
||||
name: MemoryApiName.removeIdentityMemory,
|
||||
parameters: removeIdentityJsonSchema,
|
||||
parameters: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
reason: { type: 'string' },
|
||||
},
|
||||
required: ['id', 'reason'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
identifier: 'lobe-user-memory',
|
||||
@@ -67,6 +632,3 @@ export const MemoryManifest: BuiltinToolManifest = {
|
||||
systemRole: systemPrompt,
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
/** @deprecated Use MemoryManifest instead */
|
||||
export const UserMemoryManifest = MemoryManifest;
|
||||
|
||||
@@ -27,8 +27,8 @@ export interface ThreadMetadata {
|
||||
completedAt?: string;
|
||||
/** Execution duration in milliseconds */
|
||||
duration?: number;
|
||||
/** Error message when task failed */
|
||||
error?: string;
|
||||
/** Error details when task failed */
|
||||
error?: any;
|
||||
/** Operation ID for tracking */
|
||||
operationId?: string;
|
||||
/** Task start time, used to calculate duration */
|
||||
|
||||
@@ -10,19 +10,33 @@ export enum UserMemoryContextObjectType {
|
||||
Knowledge = 'knowledge',
|
||||
Other = 'other',
|
||||
Person = 'person',
|
||||
Place = 'place'
|
||||
Place = 'place',
|
||||
}
|
||||
export const CONTEXT_OBJECT_TYPES = Object.values(UserMemoryContextObjectType);
|
||||
|
||||
export enum UserMemoryContextSubjectType {
|
||||
Item = 'item',
|
||||
Other = 'other',
|
||||
Person = 'person',
|
||||
Pet = 'pet'
|
||||
Pet = 'pet',
|
||||
}
|
||||
export const CONTEXT_SUBJECT_TYPES = Object.values(UserMemoryContextSubjectType);
|
||||
|
||||
export interface UserMemoryContext extends UserMemoryTimestamps {
|
||||
associatedObjects: { extra?: Record<string, unknown> | null, name?: string, type?: UserMemoryContextObjectType }[] | null;
|
||||
associatedSubjects: { extra?: Record<string, unknown> | null, name?: string, type?: UserMemoryContextSubjectType }[] | null;
|
||||
associatedObjects:
|
||||
| {
|
||||
extra?: Record<string, unknown> | null;
|
||||
name?: string;
|
||||
type?: UserMemoryContextObjectType;
|
||||
}[]
|
||||
| null;
|
||||
associatedSubjects:
|
||||
| {
|
||||
extra?: Record<string, unknown> | null;
|
||||
name?: string;
|
||||
type?: UserMemoryContextSubjectType;
|
||||
}[]
|
||||
| null;
|
||||
currentStatus: string | null;
|
||||
description: string | null;
|
||||
descriptionVector: number[] | null;
|
||||
@@ -97,7 +111,4 @@ export type UserMemoryPreferenceWithoutVectors = Omit<
|
||||
'conclusionDirectivesVector'
|
||||
>;
|
||||
|
||||
export type UserMemoryPreferencesListItem = Omit<
|
||||
UserMemoryPreferenceWithoutVectors,
|
||||
'suggestions'
|
||||
>;
|
||||
export type UserMemoryPreferencesListItem = Omit<UserMemoryPreferenceWithoutVectors, 'suggestions'>;
|
||||
|
||||
@@ -30,17 +30,20 @@ export enum RelationshipEnum {
|
||||
Uncle = 'uncle',
|
||||
Wife = 'wife',
|
||||
}
|
||||
export const RELATIONSHIPS = Object.values(RelationshipEnum);
|
||||
|
||||
export enum MergeStrategyEnum {
|
||||
Merge = 'merge',
|
||||
Replace = 'replace',
|
||||
}
|
||||
export const MERGE_STRATEGIES = Object.values(MergeStrategyEnum);
|
||||
|
||||
export enum IdentityTypeEnum {
|
||||
Demographic = 'demographic',
|
||||
Personal = 'personal',
|
||||
Professional = 'professional',
|
||||
}
|
||||
export const IDENTITY_TYPES = Object.values(IdentityTypeEnum);
|
||||
|
||||
export enum LayersEnum {
|
||||
Context = 'context',
|
||||
@@ -48,6 +51,7 @@ export enum LayersEnum {
|
||||
Identity = 'identity',
|
||||
Preference = 'preference',
|
||||
}
|
||||
export const MEMORY_LAYERS = Object.values(LayersEnum);
|
||||
|
||||
export enum TypesEnum {
|
||||
Activity = 'activity',
|
||||
@@ -61,6 +65,7 @@ export enum TypesEnum {
|
||||
Technology = 'technology',
|
||||
Topic = 'topic',
|
||||
}
|
||||
export const MEMORY_TYPES = Object.values(TypesEnum);
|
||||
|
||||
export enum ContextStatusEnum {
|
||||
Aborted = 'aborted',
|
||||
@@ -68,5 +73,6 @@ export enum ContextStatusEnum {
|
||||
Completed = 'completed',
|
||||
OnHold = 'on_hold',
|
||||
Ongoing = 'ongoing',
|
||||
Planned = 'planned'
|
||||
Planned = 'planned',
|
||||
}
|
||||
export const CONTEXT_STATUS = Object.values(ContextStatusEnum);
|
||||
|
||||
@@ -40,6 +40,7 @@ export default {
|
||||
'builtins.lobe-cloud-sandbox.title': 'Cloud Sandbox',
|
||||
'builtins.lobe-group-agent-builder.apiName.batchCreateAgents': 'Batch create agents',
|
||||
'builtins.lobe-group-agent-builder.apiName.createAgent': 'Create agent',
|
||||
'builtins.lobe-group-agent-builder.apiName.getAgentInfo': 'Get member info',
|
||||
'builtins.lobe-group-agent-builder.apiName.getAvailableModels': 'Get available models',
|
||||
'builtins.lobe-group-agent-builder.apiName.installPlugin': 'Install Skill',
|
||||
'builtins.lobe-group-agent-builder.apiName.inviteAgent': 'Invite member',
|
||||
|
||||
@@ -55,6 +55,8 @@ export const createRuntimeExecutors = (
|
||||
// Fallback to state's modelRuntimeConfig if not in payload
|
||||
const model = llmPayload.model || state.modelRuntimeConfig?.model;
|
||||
const provider = llmPayload.provider || state.modelRuntimeConfig?.provider;
|
||||
// Fallback to state's tools if not in payload
|
||||
const tools = llmPayload.tools || state.tools;
|
||||
|
||||
if (!model || !provider) {
|
||||
throw new Error('Model and provider are required for call_llm instruction');
|
||||
@@ -128,14 +130,14 @@ export const createRuntimeExecutors = (
|
||||
const chatPayload = {
|
||||
messages: llmPayload.messages,
|
||||
model,
|
||||
tools: llmPayload.tools,
|
||||
tools,
|
||||
};
|
||||
|
||||
log(
|
||||
`${stagePrefix} calling model-runtime chat (model: %s, messages: %d, tools: %d)`,
|
||||
model,
|
||||
llmPayload.messages.length,
|
||||
llmPayload.tools?.length ?? 0,
|
||||
tools?.length ?? 0,
|
||||
);
|
||||
|
||||
// Buffer: accumulate text and reasoning, send every 50ms
|
||||
@@ -261,7 +263,12 @@ export const createRuntimeExecutors = (
|
||||
}
|
||||
},
|
||||
onToolsCalling: async ({ toolsCalling: raw }) => {
|
||||
const payload = new ToolNameResolver().resolve(raw, state.toolManifestMap);
|
||||
const resolved = new ToolNameResolver().resolve(raw, state.toolManifestMap);
|
||||
// Add source field from toolSourceMap for routing tool execution
|
||||
const payload = resolved.map((p) => ({
|
||||
...p,
|
||||
source: state.toolSourceMap?.[p.identifier],
|
||||
}));
|
||||
// log(`[${operationLogId}][toolsCalling]`, payload);
|
||||
toolsCalling = payload;
|
||||
tool_calls = raw;
|
||||
@@ -466,6 +473,7 @@ export const createRuntimeExecutors = (
|
||||
// Execute tool using ToolExecutionService
|
||||
log(`[${operationLogId}] Executing tool ${toolName} ...`);
|
||||
const executionResult = await toolExecutionService.executeTool(chatToolPayload, {
|
||||
serverDB: ctx.serverDB,
|
||||
toolManifestMap: state.toolManifestMap,
|
||||
userId: ctx.userId,
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
import { KnowledgeBaseManifest } from '@lobechat/builtin-tool-knowledge-base';
|
||||
import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
|
||||
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
||||
import { ToolsEngine } from '@lobechat/context-engine';
|
||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type LobeToolManifest, ToolsEngine } from '@lobechat/context-engine';
|
||||
import debug from 'debug';
|
||||
|
||||
import { builtinTools } from '@/tools';
|
||||
@@ -50,11 +49,11 @@ export const createServerToolsEngine = (
|
||||
|
||||
// Get plugin manifests from installed plugins (from database)
|
||||
const pluginManifests = context.installedPlugins
|
||||
.map((plugin) => plugin.manifest as LobeChatPluginManifest)
|
||||
.map((plugin) => plugin.manifest as LobeToolManifest)
|
||||
.filter(Boolean);
|
||||
|
||||
// Get all builtin tool manifests
|
||||
const builtinManifests = builtinTools.map((tool) => tool.manifest as LobeChatPluginManifest);
|
||||
const builtinManifests = builtinTools.map((tool) => tool.manifest as LobeToolManifest);
|
||||
|
||||
// Combine all manifests
|
||||
const allManifests = [...pluginManifests, ...builtinManifests, ...additionalManifests];
|
||||
@@ -87,18 +86,27 @@ export const createServerAgentToolsEngine = (
|
||||
context: ServerAgentToolsContext,
|
||||
params: ServerCreateAgentToolsEngineParams,
|
||||
): ToolsEngine => {
|
||||
const { agentConfig, model, provider, hasEnabledKnowledgeBases = false } = params;
|
||||
const {
|
||||
additionalManifests,
|
||||
agentConfig,
|
||||
hasEnabledKnowledgeBases = false,
|
||||
model,
|
||||
provider,
|
||||
} = params;
|
||||
const searchMode = agentConfig.chatConfig?.searchMode ?? 'off';
|
||||
const isSearchEnabled = searchMode !== 'off';
|
||||
|
||||
log(
|
||||
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s',
|
||||
'Creating agent tools engine for model=%s, provider=%s, searchMode=%s, additionalManifests=%d',
|
||||
model,
|
||||
provider,
|
||||
searchMode,
|
||||
additionalManifests?.length ?? 0,
|
||||
);
|
||||
|
||||
return createServerToolsEngine(context, {
|
||||
// Pass additional manifests (e.g., LobeHub Skills)
|
||||
additionalManifests,
|
||||
// Add default tools based on configuration
|
||||
defaultToolIds: [WebBrowsingManifest.identifier, KnowledgeBaseManifest.identifier],
|
||||
// Create search-aware enableChecker for this request
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PluginEnableChecker } from '@lobechat/context-engine';
|
||||
import type { LobeToolManifest, PluginEnableChecker } from '@lobechat/context-engine';
|
||||
import type { LobeTool } from '@lobechat/types';
|
||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
|
||||
/**
|
||||
* Installed plugin with manifest
|
||||
@@ -22,7 +21,7 @@ export interface ServerAgentToolsContext {
|
||||
*/
|
||||
export interface ServerAgentToolsEngineConfig {
|
||||
/** Additional manifests to include (e.g., Klavis tools) */
|
||||
additionalManifests?: LobeChatPluginManifest[];
|
||||
additionalManifests?: LobeToolManifest[];
|
||||
/** Default tool IDs that will always be added */
|
||||
defaultToolIds?: string[];
|
||||
/** Custom enable checker for plugins */
|
||||
@@ -33,6 +32,8 @@ export interface ServerAgentToolsEngineConfig {
|
||||
* Parameters for createServerAgentToolsEngine
|
||||
*/
|
||||
export interface ServerCreateAgentToolsEngineParams {
|
||||
/** Additional manifests to include (e.g., LobeHub Skills) */
|
||||
additionalManifests?: LobeToolManifest[];
|
||||
/** Agent configuration containing plugins array */
|
||||
agentConfig: {
|
||||
/** Optional agent chat config with searchMode */
|
||||
|
||||
@@ -642,6 +642,16 @@ export const aiAgentRouter = router({
|
||||
const updatedStatus = updatedThread?.status ?? thread.status;
|
||||
const updatedTaskStatus = threadStatusToTaskStatus[updatedStatus] || 'processing';
|
||||
|
||||
// DEBUG: Log metadata for failed tasks
|
||||
if (updatedTaskStatus === 'failed') {
|
||||
console.log('[DEBUG] getSubAgentTaskStatus - failed task metadata:', {
|
||||
threadId,
|
||||
updatedStatus,
|
||||
'updatedMetadata?.error': updatedMetadata?.error,
|
||||
updatedMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Query thread messages for result content or current activity
|
||||
const threadMessages = await ctx.messageModel.query({ threadId });
|
||||
const sortedMessages = threadMessages.sort(
|
||||
|
||||
@@ -216,6 +216,161 @@ describe('AgentService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentConfig', () => {
|
||||
it('should return null if agent does not exist', async () => {
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue({});
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should support lookup by agent id', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-123',
|
||||
model: 'gpt-4',
|
||||
systemRole: 'Test role',
|
||||
};
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue({});
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('agent-123');
|
||||
|
||||
expect(mockAgentModel.getAgentConfig).toHaveBeenCalledWith('agent-123');
|
||||
expect(result?.id).toBe('agent-123');
|
||||
expect(result?.model).toBe('gpt-4');
|
||||
});
|
||||
|
||||
it('should support lookup by slug', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-123',
|
||||
model: 'claude-3',
|
||||
slug: 'my-agent',
|
||||
};
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue({});
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('my-agent');
|
||||
|
||||
expect(mockAgentModel.getAgentConfig).toHaveBeenCalledWith('my-agent');
|
||||
expect(result?.id).toBe('agent-123');
|
||||
});
|
||||
|
||||
it('should merge DEFAULT_AGENT_CONFIG and serverDefaultAgentConfig with agent config', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-1',
|
||||
systemRole: 'Custom system role',
|
||||
};
|
||||
const serverDefaultConfig = { model: 'gpt-4', params: { temperature: 0.7 } };
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue(serverDefaultConfig);
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('agent-1');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
chatConfig: DEFAULT_AGENT_CONFIG.chatConfig,
|
||||
plugins: DEFAULT_AGENT_CONFIG.plugins,
|
||||
tts: DEFAULT_AGENT_CONFIG.tts,
|
||||
model: 'gpt-4',
|
||||
params: { temperature: 0.7 },
|
||||
id: 'agent-1',
|
||||
systemRole: 'Custom system role',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default model/provider when agent has none', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-1',
|
||||
systemRole: 'Test',
|
||||
// No model or provider set
|
||||
};
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue({});
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('agent-1');
|
||||
|
||||
// Should have default model/provider from DEFAULT_AGENT_CONFIG
|
||||
expect(result?.model).toBe(DEFAULT_AGENT_CONFIG.model);
|
||||
expect(result?.provider).toBe(DEFAULT_AGENT_CONFIG.provider);
|
||||
});
|
||||
|
||||
it('should prioritize agent model/provider over defaults', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-1',
|
||||
model: 'claude-3-opus',
|
||||
provider: 'anthropic',
|
||||
};
|
||||
const serverDefaultConfig = { model: 'gpt-4', provider: 'openai' };
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue(serverDefaultConfig);
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('agent-1');
|
||||
|
||||
// Agent config should override server default
|
||||
expect(result?.model).toBe('claude-3-opus');
|
||||
expect(result?.provider).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('should merge user default agent config', async () => {
|
||||
const mockAgent = {
|
||||
id: 'agent-1',
|
||||
};
|
||||
const userDefaultConfig = { model: 'user-preferred-model', provider: 'user-provider' };
|
||||
|
||||
const mockAgentModel = {
|
||||
getAgentConfig: vi.fn().mockResolvedValue(mockAgent),
|
||||
};
|
||||
|
||||
(AgentModel as any).mockImplementation(() => mockAgentModel);
|
||||
(parseAgentConfig as any).mockReturnValue({});
|
||||
// Use mockResolvedValueOnce to avoid affecting subsequent tests
|
||||
mockUserModel.getUserSettingsDefaultAgentConfig.mockResolvedValueOnce({ config: userDefaultConfig });
|
||||
|
||||
const newService = new AgentService(mockDb, mockUserId);
|
||||
const result = await newService.getAgentConfig('agent-1');
|
||||
|
||||
// User default config should be applied
|
||||
expect(result?.model).toBe('user-preferred-model');
|
||||
expect(result?.provider).toBe('user-provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentConfigById', () => {
|
||||
it('should return null if agent does not exist', async () => {
|
||||
const mockAgentModel = {
|
||||
|
||||
@@ -17,6 +17,12 @@ import { type UpdateAgentResult } from './type';
|
||||
|
||||
const log = debug('lobe-agent:service');
|
||||
|
||||
/**
|
||||
* Agent config with required id field.
|
||||
* Used when returning agent config from database (id is always present).
|
||||
*/
|
||||
export type AgentConfigWithId = LobeAgentConfig & { id: string };
|
||||
|
||||
interface AgentWelcomeData {
|
||||
openQuestions: string[];
|
||||
welcomeMessage: string;
|
||||
@@ -78,6 +84,25 @@ export class AgentService {
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent config by ID or slug with default config merged.
|
||||
* Supports both agentId and slug lookup.
|
||||
*
|
||||
* The returned agent config is merged with:
|
||||
* 1. DEFAULT_AGENT_CONFIG (hardcoded defaults)
|
||||
* 2. Server's globalDefaultAgentConfig (from environment variable DEFAULT_AGENT_CONFIG)
|
||||
* 3. User's defaultAgentConfig (from user settings)
|
||||
* 4. The actual agent config from database
|
||||
*/
|
||||
async getAgentConfig(idOrSlug: string): Promise<AgentConfigWithId | null> {
|
||||
const [agent, defaultAgentConfig] = await Promise.all([
|
||||
this.agentModel.getAgentConfig(idOrSlug),
|
||||
this.userModel.getUserSettingsDefaultAgentConfig(),
|
||||
]);
|
||||
|
||||
return this.mergeDefaultConfig(agent, defaultAgentConfig) as AgentConfigWithId | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent config by ID with default config merged.
|
||||
*
|
||||
|
||||
@@ -231,6 +231,7 @@ export class AgentRuntimeService {
|
||||
initialMessages = [],
|
||||
appContext,
|
||||
toolManifestMap,
|
||||
toolSourceMap,
|
||||
stepCallbacks,
|
||||
} = params;
|
||||
|
||||
@@ -258,6 +259,7 @@ export class AgentRuntimeService {
|
||||
status: 'idle',
|
||||
stepCount: 0,
|
||||
toolManifestMap,
|
||||
toolSourceMap,
|
||||
tools,
|
||||
} as Partial<AgentState>;
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ export interface OperationCreationParams {
|
||||
*/
|
||||
stepCallbacks?: StepLifecycleCallbacks;
|
||||
toolManifestMap: Record<string, LobeToolManifest>;
|
||||
toolSourceMap?: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'>;
|
||||
tools?: any[];
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,22 @@ vi.mock('@/database/models/agent', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock AgentService
|
||||
vi.mock('@/server/services/agent', () => ({
|
||||
AgentService: vi.fn().mockImplementation(() => ({
|
||||
getAgentConfig: vi.fn().mockResolvedValue({
|
||||
chatConfig: {},
|
||||
files: [],
|
||||
id: 'agent-1',
|
||||
knowledgeBases: [],
|
||||
model: 'gpt-4',
|
||||
plugins: [],
|
||||
provider: 'openai',
|
||||
systemRole: 'You are a helpful assistant',
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock PluginModel
|
||||
vi.mock('@/database/models/plugin', () => ({
|
||||
PluginModel: vi.fn().mockImplementation(() => ({
|
||||
@@ -74,15 +90,19 @@ vi.mock('@/server/modules/Mecha', () => ({
|
||||
}));
|
||||
|
||||
// Mock model-bank
|
||||
vi.mock('model-bank', () => ({
|
||||
LOBE_DEFAULT_MODEL_LIST: [
|
||||
{
|
||||
abilities: { functionCall: true, video: false, vision: true },
|
||||
id: 'gpt-4',
|
||||
providerId: 'openai',
|
||||
},
|
||||
],
|
||||
}));
|
||||
vi.mock('model-bank', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('model-bank')>();
|
||||
return {
|
||||
...actual,
|
||||
LOBE_DEFAULT_MODEL_LIST: [
|
||||
{
|
||||
abilities: { functionCall: true, video: false, vision: true },
|
||||
id: 'gpt-4',
|
||||
providerId: 'openai',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
describe('AiAgentService.execAgent - threadId handling', () => {
|
||||
let service: AiAgentService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentRuntimeContext, AgentState } from '@lobechat/agent-runtime';
|
||||
import type { LobeToolManifest } from '@lobechat/context-engine';
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
import type {
|
||||
ExecAgentParams,
|
||||
@@ -10,6 +11,7 @@ import type {
|
||||
} from '@lobechat/types';
|
||||
import { ThreadStatus, ThreadType } from '@lobechat/types';
|
||||
import { nanoid } from '@lobechat/utils';
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import debug from 'debug';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
@@ -18,16 +20,43 @@ import { MessageModel } from '@/database/models/message';
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
import { ThreadModel } from '@/database/models/thread';
|
||||
import { TopicModel } from '@/database/models/topic';
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
import {
|
||||
type ServerAgentToolsContext,
|
||||
createServerAgentToolsEngine,
|
||||
serverMessagesEngine,
|
||||
} from '@/server/modules/Mecha';
|
||||
import { AgentService } from '@/server/services/agent';
|
||||
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
||||
import type { StepLifecycleCallbacks } from '@/server/services/agentRuntime/types';
|
||||
|
||||
const log = debug('lobe-server:ai-agent-service');
|
||||
|
||||
/**
|
||||
* Format error for storage in thread metadata
|
||||
* Handles Error objects which don't serialize properly with JSON.stringify
|
||||
*/
|
||||
function formatErrorForMetadata(error: unknown): Record<string, any> | undefined {
|
||||
if (!error) return undefined;
|
||||
|
||||
// Handle Error objects
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle objects with message property (like ChatMessageError)
|
||||
if (typeof error === 'object' && 'message' in error) {
|
||||
return error as Record<string, any>;
|
||||
}
|
||||
|
||||
// Fallback: wrap in object
|
||||
return { message: String(error) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal params for execAgent with step lifecycle callbacks
|
||||
* This extends the public ExecAgentParams with server-side only options
|
||||
@@ -53,6 +82,7 @@ export class AiAgentService {
|
||||
private readonly userId: string;
|
||||
private readonly db: LobeChatDatabase;
|
||||
private readonly agentModel: AgentModel;
|
||||
private readonly agentService: AgentService;
|
||||
private readonly messageModel: MessageModel;
|
||||
private readonly pluginModel: PluginModel;
|
||||
private readonly threadModel: ThreadModel;
|
||||
@@ -63,6 +93,7 @@ export class AiAgentService {
|
||||
this.userId = userId;
|
||||
this.db = db;
|
||||
this.agentModel = new AgentModel(db, userId);
|
||||
this.agentService = new AgentService(db, userId);
|
||||
this.messageModel = new MessageModel(db, userId);
|
||||
this.pluginModel = new PluginModel(db, userId);
|
||||
this.threadModel = new ThreadModel(db, userId);
|
||||
@@ -106,8 +137,8 @@ export class AiAgentService {
|
||||
|
||||
log('execAgent: identifier=%s, prompt=%s', identifier, prompt.slice(0, 50));
|
||||
|
||||
// 1. Get agent configuration from database (supports both id and slug)
|
||||
const agentConfig = await this.agentModel.getAgentConfig(identifier);
|
||||
// 1. Get agent configuration with default config merged (supports both id and slug)
|
||||
const agentConfig = await this.agentService.getAgentConfig(identifier);
|
||||
if (!agentConfig) {
|
||||
throw new Error(`Agent not found: ${identifier}`);
|
||||
}
|
||||
@@ -158,7 +189,11 @@ export class AiAgentService {
|
||||
return info?.abilities?.functionCall ?? true;
|
||||
};
|
||||
|
||||
// 5. Create tools using Server AgentToolsEngine
|
||||
// 5. Fetch LobeHub Skills manifests (temporary solution until LOBE-3517 is implemented)
|
||||
const lobehubSkillManifests = await this.fetchLobehubSkillManifests();
|
||||
log('execAgent: got %d lobehub skill manifests', lobehubSkillManifests.length);
|
||||
|
||||
// 6. Create tools using Server AgentToolsEngine
|
||||
const hasEnabledKnowledgeBases =
|
||||
agentConfig.knowledgeBases?.some((kb: { enabled?: boolean | null }) => kb.enabled === true) ??
|
||||
false;
|
||||
@@ -169,6 +204,7 @@ export class AiAgentService {
|
||||
};
|
||||
|
||||
const toolsEngine = createServerAgentToolsEngine(toolsContext, {
|
||||
additionalManifests: lobehubSkillManifests,
|
||||
agentConfig: {
|
||||
chatConfig: agentConfig.chatConfig ?? undefined,
|
||||
plugins: agentConfig.plugins ?? undefined,
|
||||
@@ -180,6 +216,8 @@ export class AiAgentService {
|
||||
|
||||
// Generate tools and manifest map
|
||||
const pluginIds = agentConfig.plugins || [];
|
||||
log('execAgent: agent configured plugins: %O', pluginIds);
|
||||
|
||||
const toolsResult = toolsEngine.generateToolsDetailed({
|
||||
model,
|
||||
provider,
|
||||
@@ -188,6 +226,12 @@ export class AiAgentService {
|
||||
|
||||
const tools = toolsResult.tools;
|
||||
|
||||
// Log detailed tools generation result
|
||||
if (toolsResult.filteredTools && toolsResult.filteredTools.length > 0) {
|
||||
log('execAgent: filtered tools: %O', toolsResult.filteredTools);
|
||||
}
|
||||
log('execAgent: enabled tool ids: %O', toolsResult.enabledToolIds);
|
||||
|
||||
// Get manifest map and convert from Map to Record
|
||||
const manifestMap = toolsEngine.getEnabledPluginManifests(pluginIds);
|
||||
const toolManifestMap: Record<string, any> = {};
|
||||
@@ -195,7 +239,20 @@ export class AiAgentService {
|
||||
toolManifestMap[id] = manifest;
|
||||
});
|
||||
|
||||
log('execAgent: generated %d tools', tools?.length ?? 0);
|
||||
// Build toolSourceMap for routing tool execution
|
||||
const toolSourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> =
|
||||
{};
|
||||
// Mark lobehub skills
|
||||
for (const manifest of lobehubSkillManifests) {
|
||||
toolSourceMap[manifest.identifier] = 'lobehubSkill';
|
||||
}
|
||||
|
||||
log(
|
||||
'execAgent: generated %d tools from %d configured plugins, %d lobehub skills',
|
||||
tools?.length ?? 0,
|
||||
pluginIds.length,
|
||||
lobehubSkillManifests.length,
|
||||
);
|
||||
|
||||
// 6. Get existing messages if provided
|
||||
let historyMessages: any[] = [];
|
||||
@@ -301,7 +358,18 @@ export class AiAgentService {
|
||||
},
|
||||
};
|
||||
|
||||
// 12. Create operation using AgentRuntimeService
|
||||
// 12. Log final operation parameters summary
|
||||
log(
|
||||
'execAgent: creating operation %s with params: model=%s, provider=%s, tools=%d, messages=%d, manifests=%d',
|
||||
operationId,
|
||||
model,
|
||||
provider,
|
||||
tools?.length ?? 0,
|
||||
processedMessages.length,
|
||||
Object.keys(toolManifestMap).length,
|
||||
);
|
||||
|
||||
// 13. Create operation using AgentRuntimeService
|
||||
// Wrap in try-catch to handle operation startup failures (e.g., QStash unavailable)
|
||||
// If createOperation fails, we still have valid messages that need error info
|
||||
try {
|
||||
@@ -320,6 +388,7 @@ export class AiAgentService {
|
||||
operationId,
|
||||
stepCallbacks,
|
||||
toolManifestMap,
|
||||
toolSourceMap,
|
||||
tools,
|
||||
userId: this.userId,
|
||||
});
|
||||
@@ -616,6 +685,11 @@ export class AiAgentService {
|
||||
}
|
||||
}
|
||||
|
||||
// Log error when task fails
|
||||
if (reason === 'error' && finalState.error) {
|
||||
console.error('execSubAgentTask: task failed for thread %s:', threadId, finalState.error);
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract summary from last assistant message and update task message content
|
||||
const lastAssistantMessage = finalState.messages
|
||||
@@ -630,12 +704,15 @@ export class AiAgentService {
|
||||
log('execSubAgentTask: updated task message %s with summary', sourceMessageId);
|
||||
}
|
||||
|
||||
// Format error for proper serialization (Error objects don't serialize with JSON.stringify)
|
||||
const formattedError = formatErrorForMetadata(finalState.error);
|
||||
|
||||
// Update Thread metadata
|
||||
await this.threadModel.update(threadId, {
|
||||
metadata: {
|
||||
completedAt,
|
||||
duration,
|
||||
error: finalState.error,
|
||||
error: formattedError,
|
||||
operationId: finalState.operationId,
|
||||
startedAt,
|
||||
totalCost: finalState.cost?.total,
|
||||
@@ -659,6 +736,98 @@ export class AiAgentService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch LobeHub Skills manifests from Market API
|
||||
* This is a temporary solution until LOBE-3517 is implemented (store skills in DB)
|
||||
*/
|
||||
private async fetchLobehubSkillManifests(): Promise<LobeToolManifest[]> {
|
||||
try {
|
||||
// 1. Get user info for trusted client token
|
||||
const user = await UserModel.findById(this.db, this.userId);
|
||||
if (!user?.email) {
|
||||
log('fetchLobehubSkillManifests: user email not found, skipping');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Generate trusted client token
|
||||
const trustedClientToken = generateTrustedClientToken({
|
||||
email: user.email,
|
||||
name: user.fullName || user.firstName || undefined,
|
||||
userId: this.userId,
|
||||
});
|
||||
|
||||
if (!trustedClientToken) {
|
||||
log('fetchLobehubSkillManifests: trusted client not configured, skipping');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Create MarketSDK instance
|
||||
const marketSDK = new MarketSDK({
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
// 4. Get user's connected skills
|
||||
const { connections } = await marketSDK.connect.listConnections();
|
||||
if (!connections || connections.length === 0) {
|
||||
log('fetchLobehubSkillManifests: no connected skills found');
|
||||
return [];
|
||||
}
|
||||
|
||||
log('fetchLobehubSkillManifests: found %d connected skills', connections.length);
|
||||
|
||||
// 5. Fetch tools for each connection and build manifests
|
||||
const manifests: LobeToolManifest[] = [];
|
||||
|
||||
for (const connection of connections) {
|
||||
try {
|
||||
// Connection returns providerId (e.g., 'twitter', 'linear'), not numeric id
|
||||
const providerId = (connection as any).providerId;
|
||||
if (!providerId) {
|
||||
log('fetchLobehubSkillManifests: connection missing providerId: %O', connection);
|
||||
continue;
|
||||
}
|
||||
const providerName =
|
||||
(connection as any).providerName || (connection as any).name || providerId;
|
||||
const icon = (connection as any).icon;
|
||||
|
||||
const { tools } = await marketSDK.skills.listTools(providerId);
|
||||
if (!tools || tools.length === 0) continue;
|
||||
|
||||
const manifest: LobeToolManifest = {
|
||||
api: tools.map((tool: any) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || { properties: {}, type: 'object' },
|
||||
})),
|
||||
identifier: providerId,
|
||||
meta: {
|
||||
avatar: icon || '🔗',
|
||||
description: `LobeHub Skill: ${providerName}`,
|
||||
tags: ['lobehub-skill', providerId],
|
||||
title: providerName,
|
||||
},
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
manifests.push(manifest);
|
||||
log(
|
||||
'fetchLobehubSkillManifests: built manifest for %s with %d tools',
|
||||
providerId,
|
||||
tools.length,
|
||||
);
|
||||
} catch (error) {
|
||||
log('fetchLobehubSkillManifests: failed to fetch tools for connection: %O', error);
|
||||
}
|
||||
}
|
||||
|
||||
return manifests;
|
||||
} catch (error) {
|
||||
log('fetchLobehubSkillManifests: error fetching skills: %O', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total tokens from AgentState usage object
|
||||
* AgentState.usage is of type Usage from @lobechat/agent-runtime
|
||||
|
||||
109
src/server/services/lobehubSkill/index.ts
Normal file
109
src/server/services/lobehubSkill/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import debug from 'debug';
|
||||
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
|
||||
const log = debug('lobe-server:lobehub-skill-service');
|
||||
|
||||
export interface LobehubSkillExecuteParams {
|
||||
args: Record<string, any>;
|
||||
provider: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export interface LobehubSkillExecuteResult {
|
||||
content: string;
|
||||
error?: { code: string; message?: string };
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class LobehubSkillService {
|
||||
private db: LobeChatDatabase;
|
||||
private userId: string;
|
||||
private marketSDK?: MarketSDK;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.db = db;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MarketSDK with trusted client token
|
||||
*/
|
||||
private async getMarketSDK(): Promise<MarketSDK | null> {
|
||||
if (this.marketSDK) return this.marketSDK;
|
||||
|
||||
try {
|
||||
const user = await UserModel.findById(this.db, this.userId);
|
||||
if (!user?.email) {
|
||||
log('getMarketSDK: user email not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const trustedClientToken = generateTrustedClientToken({
|
||||
email: user.email,
|
||||
name: user.fullName || user.firstName || undefined,
|
||||
userId: this.userId,
|
||||
});
|
||||
|
||||
if (!trustedClientToken) {
|
||||
log('getMarketSDK: trusted client not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.marketSDK = new MarketSDK({
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
return this.marketSDK;
|
||||
} catch (error) {
|
||||
log('getMarketSDK: error creating SDK: %O', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a LobeHub Skill tool
|
||||
*/
|
||||
async execute(params: LobehubSkillExecuteParams): Promise<LobehubSkillExecuteResult> {
|
||||
const { provider, toolName, args } = params;
|
||||
|
||||
log('execute: %s/%s with args: %O', provider, toolName, args);
|
||||
|
||||
const sdk = await this.getMarketSDK();
|
||||
if (!sdk) {
|
||||
return {
|
||||
content:
|
||||
'MarketSDK not available. Please ensure you are authenticated with LobeHub Market.',
|
||||
error: { code: 'MARKET_SDK_NOT_AVAILABLE' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sdk.skills.callTool(provider, {
|
||||
args,
|
||||
tool: toolName,
|
||||
});
|
||||
|
||||
log('execute: response: %O', response);
|
||||
|
||||
return {
|
||||
content: typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
||||
success: response.success,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('LobehubSkillService.execute error %s/%s: %O', provider, toolName, err);
|
||||
|
||||
return {
|
||||
content: err.message,
|
||||
error: { code: 'LOBEHUB_SKILL_ERROR', message: err.message },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { type ChatToolPayload } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import debug from 'debug';
|
||||
|
||||
import { LobehubSkillService } from '@/server/services/lobehubSkill';
|
||||
import { SearchService } from '@/server/services/search';
|
||||
|
||||
import { type IToolExecutor, type ToolExecutionContext, type ToolExecutionResult } from './types';
|
||||
@@ -19,11 +20,36 @@ export class BuiltinToolsExecutor implements IToolExecutor {
|
||||
payload: ChatToolPayload,
|
||||
context: ToolExecutionContext,
|
||||
): Promise<ToolExecutionResult> {
|
||||
const { identifier, apiName, arguments: argsStr } = payload;
|
||||
const { identifier, apiName, arguments: argsStr, source } = payload;
|
||||
const args = safeParseJSON(argsStr) || {};
|
||||
|
||||
log('Executing builtin tool: %s:%s with args: %O', identifier, apiName, args, context);
|
||||
log(
|
||||
'Executing builtin tool: %s:%s (source: %s) with args: %O',
|
||||
identifier,
|
||||
apiName,
|
||||
source,
|
||||
args,
|
||||
);
|
||||
|
||||
// Route LobeHub Skills to dedicated service
|
||||
if (source === 'lobehubSkill') {
|
||||
if (!context.serverDB || !context.userId) {
|
||||
return {
|
||||
content: 'Server context not available for LobeHub Skills execution.',
|
||||
error: { code: 'CONTEXT_NOT_AVAILABLE' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const skillService = new LobehubSkillService(context.serverDB, context.userId);
|
||||
return skillService.execute({
|
||||
args,
|
||||
provider: identifier,
|
||||
toolName: apiName,
|
||||
});
|
||||
}
|
||||
|
||||
// Default: original builtin runtime logic
|
||||
const ServerRuntime = BuiltinToolServerRuntimes[identifier];
|
||||
|
||||
if (!ServerRuntime) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { type LobeToolManifest } from '@lobechat/context-engine';
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
import { type ChatToolPayload } from '@lobechat/types';
|
||||
|
||||
export interface ToolExecutionContext {
|
||||
/** Server database for LobeHub Skills execution */
|
||||
serverDB?: LobeChatDatabase;
|
||||
toolManifestMap: Record<string, LobeToolManifest>;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
@@ -1087,12 +1087,14 @@ export const createAgentExecutors = (context: {
|
||||
}
|
||||
|
||||
if (status.status === 'failed') {
|
||||
log('[%s] Task failed: %s', taskLogId, status.error);
|
||||
// Extract error message (error is always a string in TaskStatusResult)
|
||||
const errorMessage = status.error || 'Unknown error';
|
||||
log('[%s] Task failed: %s', taskLogId, errorMessage);
|
||||
await context
|
||||
.get()
|
||||
.optimisticUpdateMessageContent(
|
||||
taskMessageId,
|
||||
`Task failed: ${status.error}`,
|
||||
`Task failed: ${errorMessage}`,
|
||||
undefined,
|
||||
{ operationId: state.operationId },
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user