opc-agent 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/index.d.ts +31 -0
- package/dist/analytics/index.js +52 -0
- package/dist/channels/voice.d.ts +43 -0
- package/dist/channels/voice.js +67 -0
- package/dist/channels/webhook.d.ts +40 -0
- package/dist/channels/webhook.js +193 -0
- package/dist/cli.js +157 -13
- package/dist/core/a2a.d.ts +46 -0
- package/dist/core/a2a.js +99 -0
- package/dist/core/hitl.d.ts +41 -0
- package/dist/core/hitl.js +100 -0
- package/dist/core/performance.d.ts +50 -0
- package/dist/core/performance.js +148 -0
- package/dist/core/room.d.ts +24 -0
- package/dist/core/room.js +97 -0
- package/dist/core/sandbox.d.ts +28 -0
- package/dist/core/sandbox.js +118 -0
- package/dist/core/versioning.d.ts +29 -0
- package/dist/core/versioning.js +114 -0
- package/dist/core/workflow.d.ts +59 -0
- package/dist/core/workflow.js +174 -0
- package/dist/i18n/index.d.ts +13 -0
- package/dist/i18n/index.js +73 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +36 -1
- package/dist/plugins/index.d.ts +47 -0
- package/dist/plugins/index.js +59 -0
- package/dist/schema/oad.d.ts +483 -15
- package/dist/schema/oad.js +53 -2
- package/dist/templates/content-writer.d.ts +36 -0
- package/dist/templates/content-writer.js +52 -0
- package/dist/templates/executive-assistant.d.ts +20 -0
- package/dist/templates/executive-assistant.js +70 -0
- package/dist/templates/financial-advisor.d.ts +15 -0
- package/dist/templates/financial-advisor.js +60 -0
- package/dist/templates/hr-recruiter.d.ts +36 -0
- package/dist/templates/hr-recruiter.js +52 -0
- package/dist/templates/legal-assistant.d.ts +15 -0
- package/dist/templates/legal-assistant.js +70 -0
- package/dist/templates/project-manager.d.ts +36 -0
- package/dist/templates/project-manager.js +52 -0
- package/dist/tools/mcp.d.ts +32 -0
- package/dist/tools/mcp.js +49 -0
- package/package.json +46 -46
- package/src/analytics/index.ts +66 -0
- package/src/channels/voice.ts +106 -0
- package/src/channels/webhook.ts +199 -0
- package/src/cli.ts +173 -16
- package/src/core/a2a.ts +143 -0
- package/src/core/hitl.ts +138 -0
- package/src/core/performance.ts +187 -0
- package/src/core/room.ts +109 -0
- package/src/core/sandbox.ts +101 -0
- package/src/core/versioning.ts +106 -0
- package/src/core/workflow.ts +235 -0
- package/src/i18n/index.ts +79 -0
- package/src/index.ts +29 -0
- package/src/plugins/index.ts +87 -0
- package/src/schema/oad.ts +59 -1
- package/src/templates/content-writer.ts +58 -0
- package/src/templates/executive-assistant.ts +71 -0
- package/src/templates/financial-advisor.ts +60 -0
- package/src/templates/hr-recruiter.ts +58 -0
- package/src/templates/legal-assistant.ts +71 -0
- package/src/templates/project-manager.ts +58 -0
- package/src/tools/mcp.ts +76 -0
- package/tests/a2a.test.ts +66 -0
- package/tests/analytics.test.ts +50 -0
- package/tests/hitl.test.ts +71 -0
- package/tests/i18n.test.ts +41 -0
- package/tests/mcp.test.ts +54 -0
- package/tests/performance.test.ts +115 -0
- package/tests/plugin.test.ts +74 -0
- package/tests/room.test.ts +106 -0
- package/tests/sandbox.test.ts +46 -0
- package/tests/templates.test.ts +77 -0
- package/tests/versioning.test.ts +75 -0
- package/tests/voice.test.ts +61 -0
- package/tests/webhook.test.ts +29 -0
- package/tests/workflow.test.ts +143 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
import type { OADDocument } from '../schema/oad';
|
|
4
|
+
|
|
5
|
+
export class BudgetAnalysisSkill extends BaseSkill {
|
|
6
|
+
name = 'budget-analysis';
|
|
7
|
+
description = 'Analyze budgets and expenses';
|
|
8
|
+
|
|
9
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
10
|
+
const lower = message.content.toLowerCase();
|
|
11
|
+
if (lower.includes('budget') || lower.includes('expense') || lower.includes('cost')) {
|
|
12
|
+
return this.match('I can help analyze your budget. Please share your income and expense categories, and I\'ll provide insights on spending patterns and savings opportunities.', 0.8);
|
|
13
|
+
}
|
|
14
|
+
if (lower.includes('save') || lower.includes('saving')) {
|
|
15
|
+
return this.match('Common savings strategies: 50/30/20 rule (needs/wants/savings), automate transfers, review subscriptions, negotiate bills, and track daily spending.', 0.75);
|
|
16
|
+
}
|
|
17
|
+
return this.noMatch();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FinancialPlanningSkill extends BaseSkill {
|
|
22
|
+
name = 'financial-planning';
|
|
23
|
+
description = 'Help with financial planning and advice';
|
|
24
|
+
|
|
25
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
const lower = message.content.toLowerCase();
|
|
27
|
+
if (lower.includes('invest') || lower.includes('portfolio')) {
|
|
28
|
+
return this.match('For investment planning, consider: risk tolerance, time horizon, diversification, asset allocation, and regular rebalancing. This is general guidance — consult a certified financial advisor for personalized advice.', 0.8);
|
|
29
|
+
}
|
|
30
|
+
if (lower.includes('retire') || lower.includes('pension')) {
|
|
31
|
+
return this.match('Retirement planning essentials: estimate target savings (25x annual expenses), maximize employer matching, consider tax-advantaged accounts, and start early for compound growth.', 0.8);
|
|
32
|
+
}
|
|
33
|
+
return this.noMatch();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createFinancialAdvisorConfig(): OADDocument {
|
|
38
|
+
return {
|
|
39
|
+
apiVersion: 'opc/v1',
|
|
40
|
+
kind: 'Agent',
|
|
41
|
+
metadata: {
|
|
42
|
+
name: 'financial-advisor',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
description: 'AI Financial Advisor - budget analysis, expense tracking, financial planning',
|
|
45
|
+
author: 'OPC',
|
|
46
|
+
license: 'Apache-2.0',
|
|
47
|
+
},
|
|
48
|
+
spec: {
|
|
49
|
+
model: 'deepseek-chat',
|
|
50
|
+
systemPrompt: 'You are a financial advisor AI. Help users with budget analysis, expense tracking, and financial planning. Always recommend consulting a certified financial advisor for binding decisions.',
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'budget-analysis', description: 'Analyze budgets and expenses' },
|
|
53
|
+
{ name: 'financial-planning', description: 'Financial planning advice' },
|
|
54
|
+
],
|
|
55
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
56
|
+
memory: { shortTerm: true, longTerm: false },
|
|
57
|
+
streaming: false,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class ResumeScreeningSkill extends BaseSkill {
|
|
6
|
+
name = 'resume-screening';
|
|
7
|
+
description = 'Screen resumes against job requirements';
|
|
8
|
+
|
|
9
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
10
|
+
const lower = message.content.toLowerCase();
|
|
11
|
+
if (lower.includes('resume') || lower.includes('cv') || lower.includes('candidate')) {
|
|
12
|
+
return this.match(
|
|
13
|
+
'I can help screen resumes. Please share the candidate\'s resume and the job requirements, and I\'ll provide an analysis.',
|
|
14
|
+
0.8,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return this.noMatch();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InterviewSchedulingSkill extends BaseSkill {
|
|
22
|
+
name = 'interview-scheduling';
|
|
23
|
+
description = 'Help schedule interviews with candidates';
|
|
24
|
+
|
|
25
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
const lower = message.content.toLowerCase();
|
|
27
|
+
if (lower.includes('schedule') || lower.includes('interview') || lower.includes('calendar')) {
|
|
28
|
+
return this.match(
|
|
29
|
+
'I can help schedule interviews. Please provide the candidate name, preferred dates, and interview format (phone/video/onsite).',
|
|
30
|
+
0.8,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return this.noMatch();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHRRecruiterConfig() {
|
|
38
|
+
return {
|
|
39
|
+
apiVersion: 'opc/v1',
|
|
40
|
+
kind: 'Agent',
|
|
41
|
+
metadata: {
|
|
42
|
+
name: 'hr-recruiter',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
description: 'HR Recruiter — resume screening, interview scheduling, candidate Q&A',
|
|
45
|
+
author: 'Deepleaper',
|
|
46
|
+
license: 'Apache-2.0',
|
|
47
|
+
},
|
|
48
|
+
spec: {
|
|
49
|
+
model: 'deepseek-chat',
|
|
50
|
+
systemPrompt: 'You are an HR recruiter assistant. Help with resume screening, interview scheduling, and answering candidate questions. Be professional and friendly.',
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'resume-screening', description: 'Screen resumes' },
|
|
53
|
+
{ name: 'interview-scheduling', description: 'Schedule interviews' },
|
|
54
|
+
],
|
|
55
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
import type { OADDocument } from '../schema/oad';
|
|
4
|
+
|
|
5
|
+
const LEGAL_TERMS: Record<string, string> = {
|
|
6
|
+
'force majeure': 'A clause that frees parties from obligations due to extraordinary events.',
|
|
7
|
+
'indemnification': 'One party agrees to compensate the other for certain damages or losses.',
|
|
8
|
+
'limitation of liability': 'A cap on the amount one party can claim from the other.',
|
|
9
|
+
'non-compete': 'Restricts a party from competing within a specified scope and timeframe.',
|
|
10
|
+
'confidentiality': 'Obligations to keep certain information private.',
|
|
11
|
+
'termination': 'Conditions under which the agreement may be ended.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class ContractReviewSkill extends BaseSkill {
|
|
15
|
+
name = 'contract-review';
|
|
16
|
+
description = 'Review contracts and identify key clauses';
|
|
17
|
+
|
|
18
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
19
|
+
const lower = message.content.toLowerCase();
|
|
20
|
+
for (const [term, explanation] of Object.entries(LEGAL_TERMS)) {
|
|
21
|
+
if (lower.includes(term)) {
|
|
22
|
+
return this.match(`📋 **${term.toUpperCase()}**: ${explanation}`, 0.85);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (lower.includes('review') || lower.includes('contract')) {
|
|
26
|
+
return this.match('I can review contracts for key clauses like force majeure, indemnification, limitation of liability, non-compete, confidentiality, and termination provisions.', 0.7);
|
|
27
|
+
}
|
|
28
|
+
return this.noMatch();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ComplianceCheckSkill extends BaseSkill {
|
|
33
|
+
name = 'compliance-check';
|
|
34
|
+
description = 'Check compliance with regulations';
|
|
35
|
+
|
|
36
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
37
|
+
const lower = message.content.toLowerCase();
|
|
38
|
+
if (lower.includes('gdpr') || lower.includes('privacy')) {
|
|
39
|
+
return this.match('GDPR compliance requires: data minimization, consent mechanisms, right to erasure, data protection officer, and breach notification within 72 hours.', 0.9);
|
|
40
|
+
}
|
|
41
|
+
if (lower.includes('compliance') || lower.includes('regulation')) {
|
|
42
|
+
return this.match('I can check compliance with GDPR, CCPA, SOX, HIPAA, and other major regulations. Please specify the regulation and context.', 0.7);
|
|
43
|
+
}
|
|
44
|
+
return this.noMatch();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createLegalAssistantConfig(): OADDocument {
|
|
49
|
+
return {
|
|
50
|
+
apiVersion: 'opc/v1',
|
|
51
|
+
kind: 'Agent',
|
|
52
|
+
metadata: {
|
|
53
|
+
name: 'legal-assistant',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
description: 'AI Legal Assistant - contract review, compliance checking, legal research',
|
|
56
|
+
author: 'OPC',
|
|
57
|
+
license: 'Apache-2.0',
|
|
58
|
+
},
|
|
59
|
+
spec: {
|
|
60
|
+
model: 'deepseek-chat',
|
|
61
|
+
systemPrompt: 'You are a legal assistant AI. Help users review contracts, check compliance, and research legal topics. Always recommend consulting a qualified attorney for binding decisions.',
|
|
62
|
+
skills: [
|
|
63
|
+
{ name: 'contract-review', description: 'Review contracts and identify key clauses' },
|
|
64
|
+
{ name: 'compliance-check', description: 'Check regulatory compliance' },
|
|
65
|
+
],
|
|
66
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
67
|
+
memory: { shortTerm: true, longTerm: false },
|
|
68
|
+
streaming: false,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class TaskTrackingSkill extends BaseSkill {
|
|
6
|
+
name = 'task-tracking';
|
|
7
|
+
description = 'Track project tasks and status';
|
|
8
|
+
|
|
9
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
10
|
+
const lower = message.content.toLowerCase();
|
|
11
|
+
if (lower.includes('task') || lower.includes('todo') || lower.includes('progress')) {
|
|
12
|
+
return this.match(
|
|
13
|
+
'I can help track tasks. Tell me the task name, assignee, and deadline, and I\'ll add it to the tracker.',
|
|
14
|
+
0.8,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return this.noMatch();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MeetingNotesSkill extends BaseSkill {
|
|
22
|
+
name = 'meeting-notes';
|
|
23
|
+
description = 'Generate and manage meeting notes';
|
|
24
|
+
|
|
25
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
const lower = message.content.toLowerCase();
|
|
27
|
+
if (lower.includes('meeting') || lower.includes('notes') || lower.includes('minutes')) {
|
|
28
|
+
return this.match(
|
|
29
|
+
'I can help with meeting notes. Share the meeting details and I\'ll create structured notes with action items.',
|
|
30
|
+
0.8,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return this.noMatch();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createProjectManagerConfig() {
|
|
38
|
+
return {
|
|
39
|
+
apiVersion: 'opc/v1',
|
|
40
|
+
kind: 'Agent',
|
|
41
|
+
metadata: {
|
|
42
|
+
name: 'project-manager',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
description: 'Project Manager — task tracking, status updates, meeting notes',
|
|
45
|
+
author: 'Deepleaper',
|
|
46
|
+
license: 'Apache-2.0',
|
|
47
|
+
},
|
|
48
|
+
spec: {
|
|
49
|
+
model: 'deepseek-chat',
|
|
50
|
+
systemPrompt: 'You are a project management assistant. Help track tasks, provide status updates, and manage meeting notes. Be organized and action-oriented.',
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'task-tracking', description: 'Track tasks' },
|
|
53
|
+
{ name: 'meeting-notes', description: 'Manage meeting notes' },
|
|
54
|
+
],
|
|
55
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/tools/mcp.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AgentContext, Message } from '../core/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP (Model Context Protocol) compatible tool interface.
|
|
5
|
+
* Tools follow the MCP standard format with JSON Schema input validation.
|
|
6
|
+
*/
|
|
7
|
+
export interface MCPToolDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: Record<string, unknown>; // JSON Schema
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MCPToolResult {
|
|
14
|
+
content: string;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MCPTool extends MCPToolDefinition {
|
|
20
|
+
execute(input: Record<string, unknown>, context?: AgentContext): Promise<MCPToolResult>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class MCPToolRegistry {
|
|
24
|
+
private tools: Map<string, MCPTool> = new Map();
|
|
25
|
+
|
|
26
|
+
register(tool: MCPTool): void {
|
|
27
|
+
this.tools.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
unregister(name: string): void {
|
|
31
|
+
this.tools.delete(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get(name: string): MCPTool | undefined {
|
|
35
|
+
return this.tools.get(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list(): MCPToolDefinition[] {
|
|
39
|
+
return Array.from(this.tools.values()).map(({ name, description, inputSchema }) => ({
|
|
40
|
+
name,
|
|
41
|
+
description,
|
|
42
|
+
inputSchema,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
has(name: string): boolean {
|
|
47
|
+
return this.tools.has(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async execute(name: string, input: Record<string, unknown>, context?: AgentContext): Promise<MCPToolResult> {
|
|
51
|
+
const tool = this.tools.get(name);
|
|
52
|
+
if (!tool) {
|
|
53
|
+
return { content: `Tool '${name}' not found`, isError: true };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return await tool.execute(input, context);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
content: `Tool execution error: ${err instanceof Error ? err.message : String(err)}`,
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an MCP tool from a simple function.
|
|
68
|
+
*/
|
|
69
|
+
export function createMCPTool(
|
|
70
|
+
name: string,
|
|
71
|
+
description: string,
|
|
72
|
+
inputSchema: Record<string, unknown>,
|
|
73
|
+
executeFn: (input: Record<string, unknown>, context?: AgentContext) => Promise<MCPToolResult>,
|
|
74
|
+
): MCPTool {
|
|
75
|
+
return { name, description, inputSchema, execute: executeFn };
|
|
76
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AgentRegistry } from '../src/core/a2a';
|
|
3
|
+
import { BaseAgent } from '../src/core/agent';
|
|
4
|
+
import { InMemoryStore } from '../src/memory';
|
|
5
|
+
|
|
6
|
+
function createTestAgent(name: string): BaseAgent {
|
|
7
|
+
const agent = new BaseAgent({ name, model: 'test', systemPrompt: `I am ${name}` }, new InMemoryStore());
|
|
8
|
+
return agent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('AgentRegistry (A2A)', () => {
|
|
12
|
+
let registry: AgentRegistry;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
registry = new AgentRegistry();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should register and discover agents', () => {
|
|
19
|
+
const agent = createTestAgent('agent-1');
|
|
20
|
+
registry.register(agent, [{ name: 'summarize', description: 'Summarize text' }]);
|
|
21
|
+
|
|
22
|
+
const all = registry.discover();
|
|
23
|
+
expect(all).toHaveLength(1);
|
|
24
|
+
expect(all[0].agentName).toBe('agent-1');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should discover by capability', () => {
|
|
28
|
+
const a1 = createTestAgent('a1');
|
|
29
|
+
const a2 = createTestAgent('a2');
|
|
30
|
+
registry.register(a1, [{ name: 'summarize', description: 'Summarize' }]);
|
|
31
|
+
registry.register(a2, [{ name: 'translate', description: 'Translate' }]);
|
|
32
|
+
|
|
33
|
+
const found = registry.discover('translate');
|
|
34
|
+
expect(found).toHaveLength(1);
|
|
35
|
+
expect(found[0].agentName).toBe('a2');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should unregister agents', () => {
|
|
39
|
+
const agent = createTestAgent('temp');
|
|
40
|
+
registry.register(agent, []);
|
|
41
|
+
registry.unregister('temp');
|
|
42
|
+
expect(registry.discover()).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle request to unknown agent', async () => {
|
|
46
|
+
const response = await registry.call('a1', 'unknown', 'test', 'hello');
|
|
47
|
+
expect(response.status).toBe('error');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should send A2A request and get response', async () => {
|
|
51
|
+
const agent = createTestAgent('responder');
|
|
52
|
+
await agent.init();
|
|
53
|
+
registry.register(agent, [{ name: 'chat', description: 'Chat' }]);
|
|
54
|
+
|
|
55
|
+
const response = await registry.call('caller', 'responder', 'chat', 'hello');
|
|
56
|
+
expect(response.status).toBe('success');
|
|
57
|
+
expect(response.payload).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should get agent by name', () => {
|
|
61
|
+
const agent = createTestAgent('finder');
|
|
62
|
+
registry.register(agent, []);
|
|
63
|
+
expect(registry.getAgent('finder')).toBeDefined();
|
|
64
|
+
expect(registry.getAgent('none')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Analytics } from '../src/analytics';
|
|
3
|
+
|
|
4
|
+
describe('Analytics', () => {
|
|
5
|
+
it('should track messages', () => {
|
|
6
|
+
const analytics = new Analytics();
|
|
7
|
+
analytics.recordMessage(100);
|
|
8
|
+
analytics.recordMessage(200);
|
|
9
|
+
const snap = analytics.getSnapshot();
|
|
10
|
+
expect(snap.messagesProcessed).toBe(2);
|
|
11
|
+
expect(snap.avgResponseTimeMs).toBe(150);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should track skill usage', () => {
|
|
15
|
+
const analytics = new Analytics();
|
|
16
|
+
analytics.recordSkillUsage('faq');
|
|
17
|
+
analytics.recordSkillUsage('faq');
|
|
18
|
+
analytics.recordSkillUsage('handoff');
|
|
19
|
+
const snap = analytics.getSnapshot();
|
|
20
|
+
expect(snap.skillUsage['faq']).toBe(2);
|
|
21
|
+
expect(snap.skillUsage['handoff']).toBe(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should track errors', () => {
|
|
25
|
+
const analytics = new Analytics();
|
|
26
|
+
analytics.recordError();
|
|
27
|
+
analytics.recordError();
|
|
28
|
+
expect(analytics.getSnapshot().errorCount).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should track token usage', () => {
|
|
32
|
+
const analytics = new Analytics();
|
|
33
|
+
analytics.recordTokens(100, 50);
|
|
34
|
+
analytics.recordTokens(200, 100);
|
|
35
|
+
const snap = analytics.getSnapshot();
|
|
36
|
+
expect(snap.tokenUsage.input).toBe(300);
|
|
37
|
+
expect(snap.tokenUsage.output).toBe(150);
|
|
38
|
+
expect(snap.tokenUsage.total).toBe(450);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should reset analytics', () => {
|
|
42
|
+
const analytics = new Analytics();
|
|
43
|
+
analytics.recordMessage(100);
|
|
44
|
+
analytics.recordError();
|
|
45
|
+
analytics.reset();
|
|
46
|
+
const snap = analytics.getSnapshot();
|
|
47
|
+
expect(snap.messagesProcessed).toBe(0);
|
|
48
|
+
expect(snap.errorCount).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { HITLManager } from '../src/core/hitl';
|
|
3
|
+
|
|
4
|
+
describe('HITLManager', () => {
|
|
5
|
+
let hitl: HITLManager;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
hitl = new HITLManager({
|
|
9
|
+
requireApproval: ['delete', 'deploy'],
|
|
10
|
+
defaultTimeoutMs: 500,
|
|
11
|
+
defaultAction: 'deny',
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should check if action needs approval', () => {
|
|
16
|
+
expect(hitl.needsApproval('delete')).toBe(true);
|
|
17
|
+
expect(hitl.needsApproval('read')).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should approve via handler', async () => {
|
|
21
|
+
hitl.setHandler(async (req) => ({
|
|
22
|
+
requestId: req.id,
|
|
23
|
+
decision: 'approve',
|
|
24
|
+
respondedAt: Date.now(),
|
|
25
|
+
timedOut: false,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const response = await hitl.requestApproval('delete', 'Delete record #123');
|
|
29
|
+
expect(response.decision).toBe('approve');
|
|
30
|
+
expect(response.timedOut).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should deny via handler', async () => {
|
|
34
|
+
hitl.setHandler(async (req) => ({
|
|
35
|
+
requestId: req.id,
|
|
36
|
+
decision: 'deny',
|
|
37
|
+
respondedAt: Date.now(),
|
|
38
|
+
timedOut: false,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const response = await hitl.requestApproval('deploy', 'Deploy to production');
|
|
42
|
+
expect(response.decision).toBe('deny');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should timeout with default action', async () => {
|
|
46
|
+
// No handler, no manual response → timeout
|
|
47
|
+
const response = await hitl.requestApproval('delete', 'Test timeout');
|
|
48
|
+
expect(response.timedOut).toBe(true);
|
|
49
|
+
expect(response.decision).toBe('deny'); // default action
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle manual respond', async () => {
|
|
53
|
+
const promise = hitl.requestApproval('delete', 'Manual test');
|
|
54
|
+
const pending = hitl.getPending();
|
|
55
|
+
expect(pending).toHaveLength(1);
|
|
56
|
+
|
|
57
|
+
hitl.respond(pending[0].id, 'approve', 'admin');
|
|
58
|
+
const response = await promise;
|
|
59
|
+
expect(response.decision).toBe('approve');
|
|
60
|
+
expect(response.respondedBy).toBe('admin');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false for unknown respond', () => {
|
|
64
|
+
expect(hitl.respond('unknown-id', 'approve')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should match wildcard approval', () => {
|
|
68
|
+
const wildcard = new HITLManager({ requireApproval: ['*'] });
|
|
69
|
+
expect(wildcard.needsApproval('anything')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { t, setLocale, getLocale, detectLocale, addMessages } from '../src/i18n';
|
|
3
|
+
|
|
4
|
+
describe('i18n', () => {
|
|
5
|
+
it('should return English messages by default', () => {
|
|
6
|
+
setLocale('en');
|
|
7
|
+
expect(t('agent.greeting')).toBe('Hello! How can I help you?');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should return Chinese messages', () => {
|
|
11
|
+
setLocale('zh-CN');
|
|
12
|
+
expect(t('agent.greeting')).toBe('您好!有什么可以帮您的?');
|
|
13
|
+
setLocale('en'); // reset
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should interpolate params', () => {
|
|
17
|
+
setLocale('en');
|
|
18
|
+
expect(t('agent.started', { name: 'TestBot' })).toBe('Agent "TestBot" started successfully');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fall back to key if not found', () => {
|
|
22
|
+
expect(t('nonexistent.key')).toBe('nonexistent.key');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should get and set locale', () => {
|
|
26
|
+
setLocale('zh-CN');
|
|
27
|
+
expect(getLocale()).toBe('zh-CN');
|
|
28
|
+
setLocale('en');
|
|
29
|
+
expect(getLocale()).toBe('en');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should add custom messages', () => {
|
|
33
|
+
addMessages('en', { 'custom.key': 'Custom value' });
|
|
34
|
+
expect(t('custom.key')).toBe('Custom value');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect locale from environment', () => {
|
|
38
|
+
const locale = detectLocale();
|
|
39
|
+
expect(['en', 'zh-CN']).toContain(locale);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MCPToolRegistry, createMCPTool } from '../src/tools/mcp';
|
|
3
|
+
|
|
4
|
+
describe('MCP Tool System', () => {
|
|
5
|
+
it('should register and list tools', () => {
|
|
6
|
+
const registry = new MCPToolRegistry();
|
|
7
|
+
const tool = createMCPTool('calculator', 'Basic calculator', {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: { expression: { type: 'string' } },
|
|
10
|
+
}, async (input) => ({ content: `Result: ${input.expression}` }));
|
|
11
|
+
|
|
12
|
+
registry.register(tool);
|
|
13
|
+
expect(registry.has('calculator')).toBe(true);
|
|
14
|
+
expect(registry.list().length).toBe(1);
|
|
15
|
+
expect(registry.list()[0].name).toBe('calculator');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should execute a tool', async () => {
|
|
19
|
+
const registry = new MCPToolRegistry();
|
|
20
|
+
registry.register(createMCPTool('echo', 'Echo tool', {}, async (input) => ({
|
|
21
|
+
content: `Echo: ${input.text}`,
|
|
22
|
+
})));
|
|
23
|
+
|
|
24
|
+
const result = await registry.execute('echo', { text: 'hello' });
|
|
25
|
+
expect(result.content).toBe('Echo: hello');
|
|
26
|
+
expect(result.isError).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return error for missing tool', async () => {
|
|
30
|
+
const registry = new MCPToolRegistry();
|
|
31
|
+
const result = await registry.execute('nonexistent', {});
|
|
32
|
+
expect(result.isError).toBe(true);
|
|
33
|
+
expect(result.content).toContain('not found');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle tool execution errors', async () => {
|
|
37
|
+
const registry = new MCPToolRegistry();
|
|
38
|
+
registry.register(createMCPTool('failing', 'Fails', {}, async () => {
|
|
39
|
+
throw new Error('boom');
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const result = await registry.execute('failing', {});
|
|
43
|
+
expect(result.isError).toBe(true);
|
|
44
|
+
expect(result.content).toContain('boom');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should unregister tools', () => {
|
|
48
|
+
const registry = new MCPToolRegistry();
|
|
49
|
+
registry.register(createMCPTool('temp', 'Temp', {}, async () => ({ content: '' })));
|
|
50
|
+
expect(registry.has('temp')).toBe(true);
|
|
51
|
+
registry.unregister('temp');
|
|
52
|
+
expect(registry.has('temp')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|