@operor/core 0.1.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/src/types.ts ADDED
@@ -0,0 +1,286 @@
1
+ // Core types for Operor
2
+
3
+ export interface IncomingMessage {
4
+ id: string;
5
+ from: string;
6
+ text: string;
7
+ timestamp: number;
8
+ channel: 'whatsapp' | 'instagram' | 'facebook' | 'sms' | 'telegram';
9
+ provider: string;
10
+ metadata?: Record<string, any>;
11
+ /** Raw file/media buffer (e.g. downloaded WhatsApp document) */
12
+ mediaBuffer?: Buffer;
13
+ /** Original file name of the attachment */
14
+ mediaFileName?: string;
15
+ /** MIME type of the attachment */
16
+ mediaMimeType?: string;
17
+ }
18
+
19
+ export interface OutgoingMessage {
20
+ to: string;
21
+ text: string;
22
+ metadata?: Record<string, any>;
23
+ /** Optional file/media buffer for document attachments */
24
+ mediaBuffer?: Buffer;
25
+ /** File name for the attachment */
26
+ mediaFileName?: string;
27
+ /** MIME type of the attachment */
28
+ mediaMimeType?: string;
29
+ }
30
+
31
+ export interface Customer {
32
+ id: string;
33
+ phone?: string;
34
+ email?: string;
35
+ name?: string;
36
+ whatsappId?: string;
37
+ instagramId?: string;
38
+ facebookId?: string;
39
+ metadata?: Record<string, any>;
40
+ lifetimeValue?: number;
41
+ firstInteraction?: Date;
42
+ lastInteraction?: Date;
43
+ }
44
+
45
+ export interface Intent {
46
+ intent: string;
47
+ confidence: number;
48
+ entities: Record<string, any>;
49
+ }
50
+
51
+ export interface IntentClassifier {
52
+ classify(message: string, agents: AgentConfig[], history?: ConversationMessage[]): Promise<Intent>;
53
+ }
54
+
55
+ export interface ToolCall {
56
+ id: string;
57
+ name: string;
58
+ params: Record<string, any>;
59
+ result?: any;
60
+ duration?: number;
61
+ success?: boolean;
62
+ error?: string;
63
+ }
64
+
65
+ export interface ConversationMessage {
66
+ role: 'user' | 'assistant';
67
+ content: string;
68
+ timestamp: number;
69
+ toolCalls?: ToolCall[];
70
+ }
71
+
72
+ export interface AgentResponse {
73
+ text: string;
74
+ toolCalls?: ToolCall[];
75
+ duration: number;
76
+ cost?: number;
77
+ usage?: {
78
+ promptTokens: number;
79
+ completionTokens: number;
80
+ };
81
+ metadata?: Record<string, any>;
82
+ mediaBuffer?: Buffer;
83
+ mediaFileName?: string;
84
+ mediaMimeType?: string;
85
+ }
86
+
87
+ export interface Tool {
88
+ name: string;
89
+ description: string;
90
+ parameters: Record<string, any>;
91
+ execute: (params: any) => Promise<any>;
92
+ }
93
+
94
+ export interface BusinessRule {
95
+ name: string;
96
+ description?: string;
97
+ condition: (context: any, toolResults: any[]) => Promise<boolean> | boolean;
98
+ action: (context: any, toolResults: any[]) => Promise<any> | any;
99
+ }
100
+
101
+ export interface AgentConfig {
102
+ name: string;
103
+ purpose?: string;
104
+ personality?: string;
105
+ triggers?: string[];
106
+ channels?: string[];
107
+ tools?: Tool[];
108
+ rules?: BusinessRule[];
109
+ guardrails?: GuardrailConfig;
110
+ knowledgeBase?: boolean; // opt-in per agent to use KB
111
+ skills?: string[]; // skill names to load (e.g. MCP servers)
112
+ systemPrompt?: string; // full system prompt for LLM
113
+ priority?: number; // routing priority (higher = preferred)
114
+ escalateTo?: string; // agent name to escalate to
115
+ /** Raw markdown body from INSTRUCTIONS.md (after YAML frontmatter) */
116
+ _rawInstructions?: string;
117
+ /** Raw content from IDENTITY.md */
118
+ _rawIdentity?: string;
119
+ /** Raw content from SOUL.md (agent-level or inherited from _defaults) */
120
+ _rawSoul?: string;
121
+ }
122
+
123
+ export interface MessageProvider {
124
+ name: string;
125
+ connect(): Promise<void>;
126
+ disconnect(): Promise<void>;
127
+ sendMessage(to: string, message: OutgoingMessage): Promise<void>;
128
+ /** Show a typing indicator to the user (e.g. "composing…" in WhatsApp, "typing…" in Telegram). */
129
+ sendTypingIndicator?(to: string): Promise<void>;
130
+ on(event: string, handler: (...args: any[]) => void): void;
131
+ off(event: string, handler: (...args: any[]) => void): void;
132
+ }
133
+
134
+ export interface Skill {
135
+ name: string;
136
+ initialize(): Promise<void>;
137
+ isReady(): boolean;
138
+ tools: Record<string, Tool>;
139
+ close?(): Promise<void>;
140
+ }
141
+
142
+ export interface LLMConfig {
143
+ provider: 'openai' | 'anthropic' | 'ollama' | 'mock';
144
+ apiKey?: string;
145
+ model?: string;
146
+ temperature?: number;
147
+ maxTokens?: number;
148
+ }
149
+
150
+ export interface LLMResponse {
151
+ text: string;
152
+ toolCalls?: ToolCall[];
153
+ usage?: {
154
+ promptTokens: number;
155
+ completionTokens: number;
156
+ totalTokens: number;
157
+ };
158
+ cost?: number;
159
+ }
160
+
161
+ export interface MemoryConfig {
162
+ type: 'memory' | 'redis' | 'postgres';
163
+ connectionString?: string;
164
+ }
165
+
166
+ export interface MemoryStore {
167
+ getCustomer(id: string): Promise<Customer | null>;
168
+ getCustomerByPhone(phone: string): Promise<Customer | null>;
169
+ upsertCustomer(customer: Customer): Promise<void>;
170
+ getHistory(customerId: string, limit?: number, agentId?: string): Promise<ConversationMessage[]>;
171
+ addMessage(customerId: string, message: ConversationMessage, agentId?: string): Promise<void>;
172
+ initialize(): Promise<void>;
173
+ close(): Promise<void>;
174
+ getSetting?(key: string): Promise<string | null>;
175
+ setSetting?(key: string, value: string | null): Promise<void>;
176
+ clearHistory?(customerId?: string, agentId?: string): Promise<{ deletedCount: number }>;
177
+ }
178
+
179
+ export interface ConversationContext {
180
+ customer: Customer;
181
+ history: ConversationMessage[];
182
+ currentMessage: IncomingMessage;
183
+ intent?: Intent;
184
+ }
185
+
186
+ export interface KnowledgeStoreConfig {
187
+ dbPath?: string; // default './operor-kb.db'
188
+ embeddingProvider?: 'openai' | 'ollama';
189
+ embeddingModel?: string; // default 'text-embedding-3-small'
190
+ embeddingApiKey?: string;
191
+ embeddingBaseUrl?: string;
192
+ dimensions?: number; // default 1536
193
+ }
194
+
195
+ export interface TrainingModeConfig {
196
+ enabled: boolean;
197
+ whitelist: string[]; // phone numbers allowed to use training commands
198
+ }
199
+
200
+ export interface GuardrailConfig {
201
+ systemRules?: string[]; // rules injected into system prompt
202
+ blockedTopics?: string[]; // topics to refuse
203
+ escalationTriggers?: string[]; // phrases that trigger human handoff
204
+ maxResponseLength?: number; // max chars in response
205
+ }
206
+
207
+ /** Runtime KB interface for training mode. Keeps core decoupled from @operor/knowledge. */
208
+ export interface KnowledgeBaseRuntime {
209
+ ingestFaq(question: string, answer: string, metadata?: Record<string, any>): Promise<{ id: string; existingMatch?: { id: string; question: string; answer: string; score: number } }>;
210
+ listDocuments(): Promise<KBDocumentSummary[]>;
211
+ deleteDocument(id: string): Promise<void>;
212
+ retrieve(query: string): Promise<{ results: KBRetrievalHit[]; context: string; isFaqMatch: boolean }>;
213
+ getStats(): Promise<{ documentCount: number; chunkCount: number; embeddingDimensions: number; dbSizeBytes: number }>;
214
+ /** Ingest a single URL (webpage). */
215
+ ingestUrl?(url: string): Promise<{ id: string; title?: string; chunks: number }>;
216
+ /** Crawl and ingest an entire website. */
217
+ ingestSite?(url: string, options?: { maxDepth?: number; maxPages?: number; onProgress?: (crawled: number, total: number, url: string) => void }): Promise<{ documents: number; chunks: number }>;
218
+ /** Ingest a file from a Buffer (e.g. media attachment from messaging). */
219
+ ingestFile?(buffer: Buffer, fileName: string): Promise<{ id: string; title?: string; chunks: number }>;
220
+ /** Rebuild all vector embeddings using the current embedding provider. */
221
+ rebuild?(): Promise<{ documentsRebuilt: number; chunksRebuilt: number; oldDimensions: number; newDimensions: number }>;
222
+ }
223
+
224
+ export interface KBDocumentSummary {
225
+ id: string;
226
+ sourceType: string;
227
+ sourceUrl?: string;
228
+ fileName?: string;
229
+ title?: string;
230
+ content: string;
231
+ createdAt: number;
232
+ }
233
+
234
+ export interface KBRetrievalHit {
235
+ chunk: { content: string };
236
+ document: { title?: string };
237
+ score: number;
238
+ }
239
+
240
+ export interface OperorConfig {
241
+ apiKey?: string;
242
+ llm?: LLMConfig;
243
+ /** LLM provider instance — used by /model commands for runtime switching */
244
+ llmProvider?: any;
245
+ memory?: MemoryStore;
246
+ intentClassifier?: IntentClassifier;
247
+ debug?: boolean;
248
+ batchWindowMs?: number;
249
+ autoAddAssistantMessages?: boolean;
250
+ knowledgeStore?: KnowledgeStoreConfig;
251
+ trainingMode?: TrainingModeConfig;
252
+ kb?: KnowledgeBaseRuntime;
253
+ /** Directory where agent definitions are stored (for /import to write files) */
254
+ agentsDir?: string;
255
+ /** Training Copilot command handler — injected when COPILOT_ENABLED=true */
256
+ copilotHandler?: {
257
+ handleCommand(command: string, args: string, adminPhone: string, reply: (text: string) => Promise<void>): Promise<void>;
258
+ };
259
+ /** Training Copilot query tracker — called after each message is processed */
260
+ copilotTracker?: {
261
+ maybeTrack(event: any): Promise<void>;
262
+ };
263
+ /** Analytics collector — attaches to Operor events to record message analytics */
264
+ analyticsCollector?: {
265
+ attach(os: any): void;
266
+ detach?(os: any): void;
267
+ };
268
+ /** Analytics store — exposed so training commands (/stats) can query metrics */
269
+ analyticsStore?: any;
270
+ /** Skills module — injected from CLI to avoid circular dependency (core cannot depend on skills) */
271
+ skillsModule?: any;
272
+ /** Project root directory — used for mcp.json path resolution */
273
+ projectRoot?: string;
274
+ }
275
+
276
+ export interface AgentDefinition {
277
+ config: AgentConfig;
278
+ systemPrompt: string;
279
+ instructionsPath: string;
280
+ }
281
+
282
+ export interface AgentEvent {
283
+ type: 'message' | 'response' | 'tool_call' | 'error';
284
+ timestamp: number;
285
+ data: any;
286
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { Operor } from '../src/index.js';
3
+ import { MockProvider } from '@operor/provider-mock';
4
+ import { MockShopifySkill } from '@operor/testing';
5
+
6
+ function sleep(ms: number): Promise<void> {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+
10
+ describe('Operor Integration', () => {
11
+ let os: Operor;
12
+ let provider: MockProvider;
13
+ let shopify: MockShopifySkill;
14
+
15
+ let messagesProcessed = 0;
16
+ let toolCallsExecuted = 0;
17
+ let rulesTriggered = 0;
18
+
19
+ beforeAll(async () => {
20
+ os = new Operor({ debug: false, batchWindowMs: 0 });
21
+ provider = new MockProvider();
22
+ shopify = new MockShopifySkill();
23
+
24
+ await os.addProvider(provider);
25
+ await os.addSkill(shopify);
26
+
27
+ os.createAgent({
28
+ name: 'Order Tracker',
29
+ purpose: 'Track orders and handle issues',
30
+ personality: 'empathetic and solution-focused',
31
+ triggers: ['order_tracking', 'general'],
32
+
33
+ tools: [shopify.tools.get_order, shopify.tools.create_discount],
34
+
35
+ rules: [
36
+ {
37
+ name: 'Auto-compensation for delays',
38
+ condition: async (_context: any, toolResults: any[]) => {
39
+ const getOrderResult = toolResults.find((t) => t.name === 'get_order');
40
+ if (!getOrderResult || !getOrderResult.success) return false;
41
+ const order = getOrderResult.result;
42
+ return order.isDelayed && order.delayDays >= 2;
43
+ },
44
+ action: async () => {
45
+ rulesTriggered++;
46
+ const discount = await shopify.tools.create_discount.execute({
47
+ percent: 10,
48
+ validDays: 30,
49
+ });
50
+ return {
51
+ type: 'discount_created',
52
+ code: discount.code,
53
+ percent: discount.percent,
54
+ validDays: discount.validDays,
55
+ };
56
+ },
57
+ },
58
+ ],
59
+ });
60
+
61
+ // Wait for all 4 messages to be processed
62
+ const allProcessed = new Promise<void>((resolve) => {
63
+ os.on('message:processed', (event: any) => {
64
+ messagesProcessed++;
65
+ if (event.toolCalls) {
66
+ toolCallsExecuted += event.toolCalls.length;
67
+ }
68
+ if (messagesProcessed >= 4) resolve();
69
+ });
70
+ });
71
+
72
+ await os.start();
73
+
74
+ // Run test scenarios sequentially
75
+ provider.simulateIncomingMessage('+1', 'Where is my order #12345?');
76
+ await sleep(100);
77
+ provider.simulateIncomingMessage('+2', 'Check order #67890');
78
+ await sleep(100);
79
+ provider.simulateIncomingMessage('+3', 'Where is order #99999?');
80
+ await sleep(100);
81
+ provider.simulateIncomingMessage('+4', 'Hello!');
82
+
83
+ await allProcessed;
84
+ }, 15000);
85
+
86
+ afterAll(async () => {
87
+ await os.stop();
88
+ });
89
+
90
+ it('should process all 4 messages', () => {
91
+ expect(messagesProcessed).toBe(4);
92
+ });
93
+
94
+ it('should execute tool calls', () => {
95
+ expect(toolCallsExecuted).toBeGreaterThanOrEqual(2);
96
+ });
97
+
98
+ it('should trigger business rules for delayed orders', () => {
99
+ expect(rulesTriggered).toBeGreaterThanOrEqual(1);
100
+ });
101
+
102
+ it('should create discounts via business rules', () => {
103
+ expect(shopify.getDiscounts().length).toBeGreaterThanOrEqual(1);
104
+ });
105
+
106
+ it('should register agents', () => {
107
+ expect(os.getAgents().length).toBe(1);
108
+ });
109
+
110
+ it('should stop cleanly', async () => {
111
+ await os.stop();
112
+ expect(os.isActive()).toBe(false);
113
+ });
114
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "dist", "test", "**/*.test.ts"]
9
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ outExtensions: () => ({ js: '.js', dts: '.d.ts' }),
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ '@operor/testing': resolve(__dirname, '../testing/src/index.ts'),
8
+ },
9
+ },
10
+ });