mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Merge remote-tracking branch 'substream/develop' into feat/set-tool-call
This commit is contained in:
@@ -120,7 +120,10 @@ const Conversations = () => {
|
||||
const notionPages = useAppSelector(state => state.notion.pages);
|
||||
const notionSummaries = useAppSelector(state => state.notion.summaries);
|
||||
const notionWorkspaceName = useAppSelector(
|
||||
state => (state.skills.skillStates?.notion as Record<string, unknown> | undefined)?.workspaceName as string | null ?? null
|
||||
state =>
|
||||
((state.skills.skillStates?.notion as Record<string, unknown> | undefined)?.workspaceName as
|
||||
| string
|
||||
| null) ?? null
|
||||
);
|
||||
|
||||
const [showPurgeConfirm, setShowPurgeConfirm] = useState(false);
|
||||
@@ -347,7 +350,7 @@ const Conversations = () => {
|
||||
|
||||
const chatMessages: ChatMessage[] = [
|
||||
...historySnapshot.map(m => ({
|
||||
role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',
|
||||
role: (m.sender === 'user' ? 'user' : 'assistant') as ChatMessage['role'],
|
||||
content: m.content,
|
||||
})),
|
||||
{ role: 'user' as const, content: processedUserContent },
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { AgentLoopService } from '../agentLoop';
|
||||
import { AgentToolRegistry } from '../agentToolRegistry';
|
||||
import { apiClient } from '../apiClient';
|
||||
import type {
|
||||
AgentToolSchema,
|
||||
AgentExecutionResult,
|
||||
AgentToolExecution,
|
||||
AgentExecutionOptions
|
||||
} from '../../types/agent';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../agentToolRegistry');
|
||||
vi.mock('../apiClient');
|
||||
|
||||
describe('AgentLoopService', () => {
|
||||
let service: AgentLoopService;
|
||||
const mockToolRegistry = AgentToolRegistry as vi.MockedClass<typeof AgentToolRegistry>;
|
||||
const mockApiClient = apiClient as { post: Mock };
|
||||
|
||||
const mockToolSchemas: AgentToolSchema[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues for a repository",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner" },
|
||||
repo: { type: "string", description: "Repository name" }
|
||||
},
|
||||
required: ["owner", "repo"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create a new Notion page",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Page title" },
|
||||
content: { type: "string", description: "Page content" }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
service = AgentLoopService.getInstance();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock implementations
|
||||
const mockRegistryInstance = {
|
||||
loadToolSchemas: vi.fn(),
|
||||
executeTool: vi.fn()
|
||||
};
|
||||
|
||||
mockToolRegistry.getInstance.mockReturnValue(mockRegistryInstance as any);
|
||||
mockRegistryInstance.loadToolSchemas.mockResolvedValue(mockToolSchemas);
|
||||
});
|
||||
|
||||
describe('executeTask', () => {
|
||||
test('should execute simple task without tool calls', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Hello! How can I help you today?',
|
||||
tool_calls: undefined
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 20,
|
||||
completion_tokens: 10,
|
||||
total_tokens: 30
|
||||
}
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Hello',
|
||||
'conv_123',
|
||||
{ maxIterations: 5, timeoutMs: 30000 }
|
||||
);
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.finalResponse).toBe('Hello! How can I help you today?');
|
||||
expect(result.iterations).toBe(1);
|
||||
expect(result.toolExecutions).toHaveLength(0);
|
||||
expect(result.executionTime).toBeGreaterThan(0);
|
||||
|
||||
// Verify API call format
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/conv_123/messages',
|
||||
expect.objectContaining({
|
||||
model: expect.any(String),
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
content: 'Hello'
|
||||
})
|
||||
]),
|
||||
tools: mockToolSchemas,
|
||||
tool_choice: 'auto'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should execute task with single tool call', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_123',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I found 3 open issues in your repository.',
|
||||
tool_calls: undefined
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 20,
|
||||
total_tokens: 70
|
||||
}
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
id: 'exec_123',
|
||||
toolName: 'list_issues',
|
||||
skillId: 'github',
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
status: 'success',
|
||||
startTime: Date.now() - 1500,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 1500,
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}'
|
||||
};
|
||||
|
||||
// Setup mocks
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
|
||||
|
||||
mockApiClient.post
|
||||
.mockResolvedValueOnce({ data: mockToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockFinalResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Show me GitHub issues',
|
||||
'conv_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.finalResponse).toBe('I found 3 open issues in your repository.');
|
||||
expect(result.iterations).toBe(2);
|
||||
expect(result.toolExecutions).toHaveLength(1);
|
||||
expect(result.toolExecutions[0].toolName).toBe('list_issues');
|
||||
expect(result.toolExecutions[0].status).toBe('success');
|
||||
|
||||
// Verify tool execution was called with correct parameters
|
||||
expect(mockRegistryInstance.executeTool).toHaveBeenCalledWith(
|
||||
'github',
|
||||
'list_issues',
|
||||
'{"owner":"user","repo":"test"}'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple tool calls in sequence', async () => {
|
||||
const mockFirstToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockSecondToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_2',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'notion_create_page',
|
||||
arguments: '{"title":"Issues Summary"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I created a summary page with your GitHub issues.',
|
||||
tool_calls: undefined
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockToolExecution1: AgentToolExecution = {
|
||||
id: 'exec_1',
|
||||
toolName: 'list_issues',
|
||||
skillId: 'github',
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
status: 'success',
|
||||
startTime: Date.now() - 2000,
|
||||
endTime: Date.now() - 1000,
|
||||
executionTimeMs: 1000,
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}'
|
||||
};
|
||||
|
||||
const mockToolExecution2: AgentToolExecution = {
|
||||
id: 'exec_2',
|
||||
toolName: 'create_page',
|
||||
skillId: 'notion',
|
||||
arguments: '{"title":"Issues Summary"}',
|
||||
status: 'success',
|
||||
startTime: Date.now() - 800,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 800,
|
||||
result: '{"page_id":"page_123"}'
|
||||
};
|
||||
|
||||
// Setup mocks
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool
|
||||
.mockResolvedValueOnce(mockToolExecution1)
|
||||
.mockResolvedValueOnce(mockToolExecution2);
|
||||
|
||||
mockApiClient.post
|
||||
.mockResolvedValueOnce({ data: mockFirstToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockSecondToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockFinalResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Get GitHub issues and create a summary page',
|
||||
'conv_123',
|
||||
{ maxIterations: 5 }
|
||||
);
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.iterations).toBe(3);
|
||||
expect(result.toolExecutions).toHaveLength(2);
|
||||
expect(result.toolExecutions[0].skillId).toBe('github');
|
||||
expect(result.toolExecutions[1].skillId).toBe('notion');
|
||||
});
|
||||
|
||||
test('should handle tool execution timeout', async () => {
|
||||
const result = await service.executeTask(
|
||||
'Test timeout',
|
||||
'conv_123',
|
||||
{ maxIterations: 1, timeoutMs: 100 } // Very short timeout
|
||||
);
|
||||
|
||||
// The timeout logic depends on how it's implemented in the actual service
|
||||
// This test may need adjustment based on the actual implementation
|
||||
expect(result.status).toBe('timeout');
|
||||
expect(result.error).toContain('timeout');
|
||||
});
|
||||
|
||||
test('should respect maximum iterations limit', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: { name: 'github_list_issues', arguments: '{}' }
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
// Mock to always return tool calls (infinite loop scenario)
|
||||
mockApiClient.post.mockResolvedValue({ data: mockToolCallResponse });
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
id: 'exec_1',
|
||||
toolName: 'list_issues',
|
||||
skillId: 'github',
|
||||
arguments: '{}',
|
||||
status: 'success',
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
result: '{}'
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Infinite loop test',
|
||||
'conv_123',
|
||||
{ maxIterations: 2, timeoutMs: 10000 }
|
||||
);
|
||||
|
||||
expect(result.status).toBe('max_iterations');
|
||||
expect(result.iterations).toBe(2);
|
||||
expect(result.error).toContain('maximum iterations');
|
||||
});
|
||||
|
||||
test('should handle tool execution error gracefully', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'invalid_tool',
|
||||
arguments: '{}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockErrorResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I encountered an error while executing the tool.',
|
||||
tool_calls: undefined
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
id: 'exec_1',
|
||||
toolName: 'invalid_tool',
|
||||
skillId: 'unknown',
|
||||
arguments: '{}',
|
||||
status: 'error',
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
errorMessage: 'Tool not found'
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
|
||||
|
||||
mockApiClient.post
|
||||
.mockResolvedValueOnce({ data: mockToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockErrorResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Test error handling',
|
||||
'conv_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.toolExecutions).toHaveLength(1);
|
||||
expect(result.toolExecutions[0].status).toBe('error');
|
||||
expect(result.toolExecutions[0].errorMessage).toBe('Tool not found');
|
||||
});
|
||||
|
||||
test('should handle API client errors', async () => {
|
||||
mockApiClient.post.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Test API error',
|
||||
'conv_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.error).toContain('Network error');
|
||||
expect(result.iterations).toBe(0);
|
||||
expect(result.toolExecutions).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should parse tool name from function name correctly', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Done',
|
||||
tool_calls: undefined
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
id: 'exec_1',
|
||||
toolName: 'list_issues',
|
||||
skillId: 'github',
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
status: 'success',
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
result: '{}'
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
|
||||
|
||||
mockApiClient.post
|
||||
.mockResolvedValueOnce({ data: mockToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockFinalResponse });
|
||||
|
||||
await service.executeTask('Test tool parsing', 'conv_123');
|
||||
|
||||
// Verify correct parsing of skill ID and tool name
|
||||
expect(mockRegistryInstance.executeTool).toHaveBeenCalledWith(
|
||||
'github',
|
||||
'list_issues',
|
||||
'{"owner":"user","repo":"test"}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('singleton behavior', () => {
|
||||
test('should return the same instance', () => {
|
||||
const instance1 = AgentLoopService.getInstance();
|
||||
const instance2 = AgentLoopService.getInstance();
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task execution options', () => {
|
||||
test('should use default options when none provided', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Test response'
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await service.executeTask('Test', 'conv_123');
|
||||
|
||||
// Should complete successfully with defaults
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.executionTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should respect custom execution options', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Test response'
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const customOptions: AgentExecutionOptions = {
|
||||
maxIterations: 3,
|
||||
timeoutMs: 5000,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7
|
||||
};
|
||||
|
||||
const result = await service.executeTask('Test', 'conv_123', customOptions);
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
|
||||
// Verify custom options were passed to API
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/conv_123/messages',
|
||||
expect.objectContaining({
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,367 @@
|
||||
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { AgentToolRegistry } from '../agentToolRegistry';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// Mock Tauri invoke
|
||||
vi.mock('@tauri-apps/api/core');
|
||||
|
||||
describe('AgentToolRegistry', () => {
|
||||
let service: AgentToolRegistry;
|
||||
const mockInvoke = invoke as Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
service = AgentToolRegistry.getInstance();
|
||||
vi.clearAllMocks();
|
||||
service.clearCache(); // Clear cache between tests
|
||||
});
|
||||
|
||||
describe('loadToolSchemas', () => {
|
||||
test('should load tool schemas from Tauri using ZeroClaw format', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues for a repository",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner" },
|
||||
repo: { type: "string", description: "Repository name" }
|
||||
},
|
||||
required: ["owner", "repo"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create a new Notion page",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Page title" },
|
||||
content: { type: "string", description: "Page content" }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
|
||||
const schemas = await service.loadToolSchemas();
|
||||
|
||||
expect(schemas).toHaveLength(2);
|
||||
expect(schemas[0].function.name).toBe("github_list_issues");
|
||||
expect(schemas[1].function.name).toBe("notion_create_page");
|
||||
expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
|
||||
});
|
||||
|
||||
test('should cache tool schemas to avoid repeated calls', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
|
||||
// First call
|
||||
const schemas1 = await service.loadToolSchemas();
|
||||
// Second call
|
||||
const schemas2 = await service.loadToolSchemas();
|
||||
|
||||
expect(schemas1).toEqual(schemas2);
|
||||
// Should only invoke Tauri once due to caching (TTL = 5 minutes)
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should force reload when requested', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
|
||||
// First call
|
||||
await service.loadToolSchemas();
|
||||
// Force reload
|
||||
await service.loadToolSchemas(true);
|
||||
|
||||
// Should invoke Tauri twice
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should handle empty tool schema response', async () => {
|
||||
mockInvoke.mockResolvedValue([]);
|
||||
|
||||
const schemas = await service.loadToolSchemas();
|
||||
|
||||
expect(schemas).toHaveLength(0);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
|
||||
});
|
||||
|
||||
test('should throw error when Tauri command fails', async () => {
|
||||
const errorMessage = 'Failed to load tool schemas';
|
||||
mockInvoke.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await expect(service.loadToolSchemas()).rejects.toThrow(`Failed to load tool schemas: Error: ${errorMessage}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeTool', () => {
|
||||
test('should execute tool using ZeroClaw format with success', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
output: '{"issues": [{"title": "Bug fix", "number": 1}]}',
|
||||
error: null,
|
||||
execution_time: 1500
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.executeTool(
|
||||
'github',
|
||||
'list_issues',
|
||||
'{"owner":"user","repo":"test"}'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.result).toBe(mockResult.output);
|
||||
expect(result.executionTimeMs).toBe(1500);
|
||||
expect(result.toolName).toBe('list_issues');
|
||||
expect(result.skillId).toBe('github');
|
||||
|
||||
// Verify correct tool_id format and arguments
|
||||
expect(mockInvoke).toHaveBeenCalledWith('runtime_execute_tool', {
|
||||
toolId: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle tool execution failure', async () => {
|
||||
const mockResult = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'Tool not found: invalid_tool',
|
||||
execution_time: 100
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.executeTool(
|
||||
'invalid',
|
||||
'tool',
|
||||
'{}'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.errorMessage).toBe('Tool not found: invalid_tool');
|
||||
expect(result.result).toBe('Tool not found: invalid_tool');
|
||||
expect(result.executionTimeMs).toBe(100);
|
||||
});
|
||||
|
||||
test('should handle tool execution without execution_time', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
output: 'Success',
|
||||
error: null
|
||||
// No execution_time provided
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await service.executeTool('test', 'tool', '{}');
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.executionTimeMs).toBeGreaterThan(0);
|
||||
expect(result.executionTimeMs).toBeLessThanOrEqual(endTime - startTime + 10); // Allow small margin
|
||||
});
|
||||
|
||||
test('should handle Tauri invoke exception', async () => {
|
||||
const errorMessage = 'Network error';
|
||||
mockInvoke.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const result = await service.executeTool('test', 'tool', '{}');
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.errorMessage).toBe(errorMessage);
|
||||
expect(result.result).toBe(errorMessage);
|
||||
expect(result.executionTimeMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should generate unique execution IDs', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
output: 'test',
|
||||
error: null,
|
||||
execution_time: 100
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
const result1 = await service.executeTool('test', 'tool1', '{}');
|
||||
const result2 = await service.executeTool('test', 'tool2', '{}');
|
||||
|
||||
expect(result1.id).not.toBe(result2.id);
|
||||
expect(result1.id).toMatch(/^exec_\d+_[a-z0-9]+$/);
|
||||
expect(result2.id).toMatch(/^exec_\d+_[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool management methods', () => {
|
||||
beforeEach(async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "github_create_issue",
|
||||
description: "Create GitHub issue",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create Notion page",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
await service.loadToolSchemas();
|
||||
});
|
||||
|
||||
test('getToolByName should find tool by name', () => {
|
||||
const tool = service.getToolByName('github_list_issues');
|
||||
|
||||
expect(tool).toBeDefined();
|
||||
expect(tool?.function.name).toBe('github_list_issues');
|
||||
expect(tool?.function.description).toBe('List GitHub issues');
|
||||
});
|
||||
|
||||
test('getToolByName should return undefined for non-existent tool', () => {
|
||||
const tool = service.getToolByName('non_existent_tool');
|
||||
|
||||
expect(tool).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getAllTools should return all loaded tools', () => {
|
||||
const tools = service.getAllTools();
|
||||
|
||||
expect(tools).toHaveLength(3);
|
||||
expect(tools.map(t => t.function.name)).toEqual([
|
||||
'github_list_issues',
|
||||
'github_create_issue',
|
||||
'notion_create_page'
|
||||
]);
|
||||
});
|
||||
|
||||
test('getToolsBySkill should organize tools by skill ID', () => {
|
||||
const toolsBySkill = service.getToolsBySkill();
|
||||
|
||||
expect(toolsBySkill).toHaveProperty('github');
|
||||
expect(toolsBySkill).toHaveProperty('notion');
|
||||
expect(toolsBySkill.github).toHaveLength(2);
|
||||
expect(toolsBySkill.notion).toHaveLength(1);
|
||||
|
||||
expect(toolsBySkill.github.map(t => t.function.name)).toEqual([
|
||||
'github_list_issues',
|
||||
'github_create_issue'
|
||||
]);
|
||||
expect(toolsBySkill.notion[0].function.name).toBe('notion_create_page');
|
||||
});
|
||||
|
||||
test('getToolStats should return accurate statistics', () => {
|
||||
const stats = service.getToolStats();
|
||||
|
||||
expect(stats.totalTools).toBe(3);
|
||||
expect(stats.skillCount).toBe(2);
|
||||
expect(stats.categories).toHaveProperty('GitHub', 2);
|
||||
expect(stats.categories).toHaveProperty('Notion', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('helper methods', () => {
|
||||
test('extractSkillIdFromToolName should parse skill ID correctly', () => {
|
||||
// Use reflection to access private method
|
||||
const extractMethod = (service as any).extractSkillIdFromToolName.bind(service);
|
||||
|
||||
expect(extractMethod('github_list_issues')).toBe('github');
|
||||
expect(extractMethod('notion_create_page')).toBe('notion');
|
||||
expect(extractMethod('complex_skill_name_tool_name')).toBe('complex_skill_name_tool');
|
||||
expect(extractMethod('invalid_format')).toBe('invalid');
|
||||
expect(extractMethod('no_underscore')).toBeNull();
|
||||
});
|
||||
|
||||
test('extractCategoryFromSkillId should categorize skills correctly', () => {
|
||||
// Use reflection to access private method
|
||||
const extractMethod = (service as any).extractCategoryFromSkillId.bind(service);
|
||||
|
||||
expect(extractMethod('github')).toBe('GitHub');
|
||||
expect(extractMethod('github_enterprise')).toBe('GitHub');
|
||||
expect(extractMethod('notion')).toBe('Notion');
|
||||
expect(extractMethod('telegram')).toBe('Telegram');
|
||||
expect(extractMethod('gmail')).toBe('Email');
|
||||
expect(extractMethod('calendar')).toBe('Calendar');
|
||||
expect(extractMethod('slack')).toBe('Slack');
|
||||
expect(extractMethod('crypto_wallet')).toBe('Crypto');
|
||||
expect(extractMethod('unknown_skill')).toBe('Other');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearCache', () => {
|
||||
test('should clear cached tool schemas', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
|
||||
// Load schemas
|
||||
await service.loadToolSchemas();
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clear cache
|
||||
service.clearCache();
|
||||
|
||||
// Load again - should call Tauri again
|
||||
await service.loadToolSchemas();
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Agent Tool Registry Service
|
||||
*
|
||||
* Builds on top of the existing skill system to provide agent-compatible
|
||||
* tool discovery and execution. Uses ZeroClaw format compatibility commands:
|
||||
* - runtime_get_tool_schemas: Get all tools in OpenAI-compatible format
|
||||
* - runtime_execute_tool: Execute a tool with enhanced validation and timing
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type {
|
||||
AgentToolSchema,
|
||||
AgentToolExecution,
|
||||
IAgentToolRegistry
|
||||
} from '../types/agent';
|
||||
|
||||
// ZeroClaw format types from Rust
|
||||
interface ZeroClawToolSchema {
|
||||
type: string;
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface ZeroClawToolResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
execution_time?: number;
|
||||
}
|
||||
|
||||
export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
private static instance: AgentToolRegistry;
|
||||
private toolSchemas: AgentToolSchema[] = [];
|
||||
private lastLoadTime = 0;
|
||||
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
static getInstance(): AgentToolRegistry {
|
||||
if (!this.instance) {
|
||||
this.instance = new AgentToolRegistry();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tool schemas from the skill system using ZeroClaw format
|
||||
*/
|
||||
async loadToolSchemas(forceReload = false): Promise<AgentToolSchema[]> {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached tools if still fresh
|
||||
if (!forceReload && this.toolSchemas.length > 0 && (now - this.lastLoadTime) < this.CACHE_TTL) {
|
||||
return this.toolSchemas;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔧 Loading tool schemas from skill system (ZeroClaw format)...');
|
||||
|
||||
// Call ZeroClaw format command to get tools in OpenAI-compatible format
|
||||
const zeroClawTools = await invoke<ZeroClawToolSchema[]>('runtime_get_tool_schemas');
|
||||
|
||||
console.log(`🔧 Loaded ${zeroClawTools.length} tools in ZeroClaw format`);
|
||||
|
||||
// Tools are already in OpenAI format, just map to our interface
|
||||
this.toolSchemas = zeroClawTools.map(tool => ({
|
||||
type: tool.type,
|
||||
function: {
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters
|
||||
}
|
||||
}));
|
||||
|
||||
this.lastLoadTime = now;
|
||||
|
||||
console.log(`✅ Tool registry updated: ${this.toolSchemas.length} tools available`);
|
||||
|
||||
return this.toolSchemas;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load tool schemas:', error);
|
||||
throw new Error(`Failed to load tool schemas: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool using ZeroClaw format with enhanced validation
|
||||
*/
|
||||
async executeTool(
|
||||
skillId: string,
|
||||
toolName: string,
|
||||
toolArguments: string
|
||||
): Promise<AgentToolExecution> {
|
||||
const startTime = Date.now();
|
||||
const executionId = `exec_${startTime}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Create tool ID in format expected by runtime_execute_tool
|
||||
const toolId = `${skillId}_${toolName}`;
|
||||
|
||||
console.log(`🚀 [TOOL EXECUTION START] Executing tool: ${toolId}`);
|
||||
console.log(`📝 [ARGUMENTS] Raw arguments:`, {
|
||||
arguments: toolArguments,
|
||||
type: typeof toolArguments,
|
||||
length: toolArguments?.length,
|
||||
isString: typeof toolArguments === 'string',
|
||||
parsed: (() => {
|
||||
try {
|
||||
return typeof toolArguments === 'string' ? JSON.parse(toolArguments) : toolArguments;
|
||||
} catch (e) {
|
||||
return 'Failed to parse: ' + e;
|
||||
}
|
||||
})()
|
||||
});
|
||||
|
||||
const execution: AgentToolExecution = {
|
||||
id: executionId,
|
||||
toolName,
|
||||
skillId,
|
||||
arguments: toolArguments,
|
||||
status: 'running',
|
||||
startTime
|
||||
};
|
||||
|
||||
try {
|
||||
// Call ZeroClaw format command with enhanced validation and timing
|
||||
console.log(`🔧 [BEFORE INVOKE] Calling runtime_execute_tool with:`);
|
||||
console.log(` toolId: "${toolId}"`);
|
||||
console.log(` args: ${toolArguments}`);
|
||||
console.log(` args type: ${typeof toolArguments}`);
|
||||
|
||||
const result = await invoke<ZeroClawToolResult>('runtime_execute_tool', {
|
||||
toolId: toolId, // Use camelCase as expected by current Rust version
|
||||
args: toolArguments // Use "args" instead of "arguments"
|
||||
});
|
||||
|
||||
console.log(`🔧 [AFTER INVOKE] Tool execution result:`, result);
|
||||
|
||||
execution.endTime = Date.now();
|
||||
// Use execution time from Rust if available, otherwise calculate locally
|
||||
execution.executionTimeMs = result.execution_time || (execution.endTime - execution.startTime);
|
||||
|
||||
if (!result.success) {
|
||||
execution.status = 'error';
|
||||
execution.errorMessage = result.error || 'Unknown error occurred';
|
||||
execution.result = execution.errorMessage;
|
||||
|
||||
console.log(`❌ Tool execution failed: ${toolName} (${execution.executionTimeMs}ms)`);
|
||||
console.log(`❌ Error:`, execution.errorMessage);
|
||||
} else {
|
||||
execution.status = 'success';
|
||||
execution.result = result.output;
|
||||
|
||||
console.log(`✅ Tool execution completed: ${toolName} (${execution.executionTimeMs}ms)`);
|
||||
}
|
||||
|
||||
return execution;
|
||||
|
||||
} catch (error) {
|
||||
execution.endTime = Date.now();
|
||||
execution.executionTimeMs = execution.endTime - execution.startTime;
|
||||
execution.status = 'error';
|
||||
execution.errorMessage = error instanceof Error ? error.message : String(error);
|
||||
execution.result = execution.errorMessage;
|
||||
|
||||
console.error(`❌ Tool execution error: ${toolName}`, error);
|
||||
|
||||
return execution;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tool by name
|
||||
*/
|
||||
getToolByName(toolName: string): AgentToolSchema | undefined {
|
||||
return this.toolSchemas.find(tool => tool.function.name === toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tools
|
||||
*/
|
||||
getAllTools(): AgentToolSchema[] {
|
||||
return [...this.toolSchemas];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools organized by skill
|
||||
*/
|
||||
getToolsBySkill(): Record<string, AgentToolSchema[]> {
|
||||
const toolsBySkill: Record<string, AgentToolSchema[]> = {};
|
||||
|
||||
for (const tool of this.toolSchemas) {
|
||||
// Extract skill ID from tool name (format: skillId_toolName)
|
||||
const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown';
|
||||
|
||||
if (!toolsBySkill[skillId]) {
|
||||
toolsBySkill[skillId] = [];
|
||||
}
|
||||
toolsBySkill[skillId].push(tool);
|
||||
}
|
||||
|
||||
return toolsBySkill;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool execution statistics
|
||||
*/
|
||||
getToolStats(): {
|
||||
totalTools: number;
|
||||
skillCount: number;
|
||||
categories: Record<string, number>;
|
||||
} {
|
||||
const categories: Record<string, number> = {};
|
||||
const skills = new Set<string>();
|
||||
|
||||
for (const tool of this.toolSchemas) {
|
||||
const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown';
|
||||
skills.add(skillId);
|
||||
|
||||
// Categorize by skill name
|
||||
const category = this.extractCategoryFromSkillId(skillId);
|
||||
categories[category] = (categories[category] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalTools: this.toolSchemas.length,
|
||||
skillCount: skills.size,
|
||||
categories
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the tool registry cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.toolSchemas = [];
|
||||
this.lastLoadTime = 0;
|
||||
console.log('🔧 Tool registry cache cleared');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Private Helper Methods
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extract skill ID from tool name (format: skillId_toolName)
|
||||
*/
|
||||
private extractSkillIdFromToolName(toolName: string): string | null {
|
||||
const underscoreIndex = toolName.lastIndexOf('_');
|
||||
if (underscoreIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
return toolName.substring(0, underscoreIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract category name from skill ID for organization
|
||||
*/
|
||||
private extractCategoryFromSkillId(skillId: string): string {
|
||||
// Common skill naming patterns
|
||||
if (skillId.includes('github') || skillId.includes('git')) return 'GitHub';
|
||||
if (skillId.includes('notion')) return 'Notion';
|
||||
if (skillId.includes('telegram') || skillId.includes('tg')) return 'Telegram';
|
||||
if (skillId.includes('email') || skillId.includes('gmail')) return 'Email';
|
||||
if (skillId.includes('calendar')) return 'Calendar';
|
||||
if (skillId.includes('slack')) return 'Slack';
|
||||
if (skillId.includes('discord')) return 'Discord';
|
||||
if (skillId.includes('twitter') || skillId.includes('x')) return 'Social';
|
||||
if (skillId.includes('file') || skillId.includes('fs')) return 'File System';
|
||||
if (skillId.includes('crypto') || skillId.includes('blockchain')) return 'Crypto';
|
||||
if (skillId.includes('ai') || skillId.includes('ml')) return 'AI/ML';
|
||||
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,23 @@ import { apiClient } from '../apiClient';
|
||||
|
||||
export type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
/** tool_call_id for role=tool messages */
|
||||
tool_call_id?: string;
|
||||
/** tool_calls emitted by the assistant */
|
||||
tool_calls?: ToolCall[];
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: { name: string; arguments: string };
|
||||
}
|
||||
|
||||
// ── Tool calling types (OpenAI-compatible) ───────────────────────────────────
|
||||
export interface ChatMessage {
|
||||
role: ChatRole;
|
||||
content: string | null;
|
||||
tool_calls?: ToolCall[];
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
export interface ToolFunction {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: 'object';
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
parameters: any;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
@@ -30,23 +28,14 @@ export interface Tool {
|
||||
function: ToolFunction;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
tools?: Tool[];
|
||||
tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
tools?: Tool[];
|
||||
tool_choice?: 'none' | 'auto' | 'required';
|
||||
}
|
||||
|
||||
export interface TextCompletionRequest {
|
||||
@@ -116,16 +105,12 @@ export const inferenceApi = {
|
||||
},
|
||||
|
||||
/** POST /openai/v1/chat/completions — create a chat completion */
|
||||
createChatCompletion: async (
|
||||
body: ChatCompletionRequest
|
||||
): Promise<ChatCompletionResponse> => {
|
||||
createChatCompletion: async (body: ChatCompletionRequest): Promise<ChatCompletionResponse> => {
|
||||
return apiClient.post<ChatCompletionResponse>('/openai/v1/chat/completions', body);
|
||||
},
|
||||
|
||||
/** POST /openai/v1/completions — create a text completion */
|
||||
createCompletion: async (
|
||||
body: TextCompletionRequest
|
||||
): Promise<TextCompletionResponse> => {
|
||||
createCompletion: async (body: TextCompletionRequest): Promise<TextCompletionResponse> => {
|
||||
return apiClient.post<TextCompletionResponse>('/openai/v1/completions', body);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export class DaemonHealthService {
|
||||
// console.log('[DaemonHealth] Setting up alphahuman:health event listener');
|
||||
|
||||
this.healthEventListener = await listen<unknown>('alphahuman:health', event => {
|
||||
console.log('[DaemonHealth] Received health event:', event.payload);
|
||||
// console.log('[DaemonHealth] Received health event:', event.payload);
|
||||
|
||||
const healthSnapshot = this.parseHealthSnapshot(event.payload);
|
||||
if (healthSnapshot) {
|
||||
|
||||
@@ -53,6 +53,7 @@ const threadPersistConfig = {
|
||||
whitelist: ['panelWidth', 'lastViewedAt', 'threads', 'messagesByThreadId', 'selectedThreadId'],
|
||||
};
|
||||
|
||||
|
||||
const persistedAuthReducer = persistReducer(authPersistConfig, authReducer);
|
||||
const persistedAiReducer = persistReducer(aiPersistConfig, aiReducer);
|
||||
const persistedSkillsReducer = persistReducer(skillsPersistConfig, skillsReducer);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { threadApi } from '../services/api/threadApi';
|
||||
import type { Thread, ThreadMessage } from '../types/thread';
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
import type { RootState } from './index';
|
||||
|
||||
interface ThreadState {
|
||||
// Existing local data (will be persisted)
|
||||
@@ -125,7 +126,7 @@ export const sendMessage = createAsyncThunk(
|
||||
// 3. Send to API with processed message (disable injection in threadApi to avoid double injection)
|
||||
const data = await threadApi.sendMessage(processedMessage, threadId, { injectSoul: false });
|
||||
|
||||
// 3. For now, we'll handle AI response via the existing inference API
|
||||
// 4. For now, we'll handle AI response via the existing inference API
|
||||
// The AI response will be added separately via addInferenceResponse
|
||||
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user