pacs-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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * PACS Core — OpenClaw Bridge Module
3
+ * Integrates PACS with the OpenClaw agent framework
4
+ *
5
+ * Modes:
6
+ * - "standalone": PACS runs independently, OpenClaw is just the UI
7
+ * - "integrated": PACS and OpenClaw agents coexist, PACS gets first pick
8
+ */
9
+
10
+ import { parse, containsTrigger, detectLearnMode } from './trigger-parser.js';
11
+ import { listAgents, getAgent, registerAgent } from './agent-registry.js';
12
+ import { searchFacts, addFact, readMemory } from './memory-store.js';
13
+ import LearnMode from './learn-mode.js';
14
+ import RoutingEngine from './routing-engine.js';
15
+ import AgentCreator from './agent-creator.js';
16
+
17
+ /** @type {object|null} */
18
+ let _openClawConfig = null;
19
+
20
+ /** @type {object|null} OpenClaw agent API (when available) */
21
+ let _openClawAgents = null;
22
+
23
+ /**
24
+ * PACS keywords / command prefixes that indicate a PACS query.
25
+ * @type {string[]}
26
+ */
27
+ const PACS_TRIGGERS = ['MERKE', 'SUCHE', 'LERNE', 'AGENT', 'ROUTING', 'REGELN', 'MEMORY'];
28
+
29
+ /**
30
+ * Detect whether a user message is a PACS command or a general OpenClaw task.
31
+ * @param {string} message
32
+ * @returns {boolean}
33
+ */
34
+ export function isPACSQuery(message) {
35
+ if (!message || typeof message !== 'string') return false;
36
+ const upper = message.trim().toUpperCase();
37
+ // Explicit trigger words
38
+ if (PACS_TRIGGERS.some((t) => upper.startsWith(t))) return true;
39
+ // Trigger-parser detection
40
+ if (containsTrigger(message)) return true;
41
+ // Learn mode intent
42
+ if (detectLearnMode(message)) return true;
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Initialize PACS inside OpenClaw.
48
+ * Call once at OpenClaw startup.
49
+ *
50
+ * @param {object} openClawConfig - { pacs: { enabled, mode } }
51
+ * @param {object} [openClawAgentApi] - Optional OpenClaw agent API for fallback
52
+ */
53
+ export function init(openClawConfig = {}, openClawAgentApi = null) {
54
+ _openClawConfig = openClawConfig;
55
+ _openClawAgents = openClawAgentApi;
56
+
57
+ const pacsConfig = openClawConfig.pacs || {};
58
+ const mode = pacsConfig.mode || 'integrated';
59
+ const enabled = pacsConfig.enabled !== false;
60
+
61
+ if (!enabled) {
62
+ console.warn('[PACS Bridge] PACS is disabled in config.');
63
+ return { ok: false, reason: 'disabled' };
64
+ }
65
+
66
+ console.log(`[PACS Bridge] Initialized in "${mode}" mode.`);
67
+ return { ok: true, mode };
68
+ }
69
+
70
+ /**
71
+ * Main entry — PACS processes the user message, routes to agents, returns response.
72
+ *
73
+ * @param {string} userMessage
74
+ * @returns {Promise<object>} { response, routedTo, fallback }
75
+ */
76
+ export async function query(userMessage) {
77
+ if (!isPACSQuery(userMessage)) {
78
+ return fallbackToOpenClawAgents(userMessage);
79
+ }
80
+
81
+ const mode = (_openClawConfig?.pacs?.mode) || 'integrated';
82
+
83
+ try {
84
+ const trigger = parse(userMessage);
85
+ const upper = userMessage.trim().toUpperCase();
86
+
87
+ // --- MERKE / learn a fact ---
88
+ if (upper.startsWith('MERKE') || upper.startsWith('LERNE')) {
89
+ const fact = userMessage
90
+ .replace(/^(MERKE|LERNE)\s*/i, '')
91
+ .trim();
92
+ if (fact) {
93
+ addFact(fact);
94
+ return {
95
+ response: `✓ Merke: ${fact}`,
96
+ routedTo: 'PACS-memory',
97
+ fallback: false,
98
+ };
99
+ }
100
+ }
101
+
102
+ // --- SUCHE / search memory ---
103
+ if (upper.startsWith('SUCHE')) {
104
+ const query2 = userMessage
105
+ .replace(/^(SUCHE|SUCH)\s*/i, '')
106
+ .trim();
107
+ const results = searchFacts(query2);
108
+ if (results.length === 0) {
109
+ return {
110
+ response: `Keine Ergebnisse für "${query2}".`,
111
+ routedTo: 'PACS-memory',
112
+ fallback: false,
113
+ };
114
+ }
115
+ return {
116
+ response: `Gefunden:\n${results.map((r, i) => `${i + 1}. ${r}`).join('\n')}`,
117
+ routedTo: 'PACS-memory',
118
+ fallback: false,
119
+ };
120
+ }
121
+
122
+ // --- AGENT commands ---
123
+ if (upper.startsWith('AGENT')) {
124
+ return handleAgentCommand(userMessage);
125
+ }
126
+
127
+ // --- Learn mode ---
128
+ if (detectLearnMode(userMessage)) {
129
+ return handleLearnModeCommand(userMessage);
130
+ }
131
+
132
+ // --- Routing engine ---
133
+ const routingEngine = new RoutingEngine();
134
+ const route = routingEngine.route(userMessage);
135
+ if (route && route.agent) {
136
+ return {
137
+ response: `→ Agent "${route.agent}" (Confidence: ${route.confidence})`,
138
+ routedTo: route.agent,
139
+ fallback: false,
140
+ };
141
+ }
142
+
143
+ // No PACS handler — fallback to OpenClaw agents
144
+ if (mode === 'integrated') {
145
+ return fallbackToOpenClawAgents(userMessage);
146
+ }
147
+
148
+ return {
149
+ response: 'PACS verstanden, aber keine passende Aktion gefunden.',
150
+ routedTo: null,
151
+ fallback: false,
152
+ };
153
+ } catch (err) {
154
+ console.error('[PACS Bridge] query error:', err);
155
+ return {
156
+ response: `Fehler: ${err.message}`,
157
+ routedTo: null,
158
+ fallback: false,
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Fallback: delegate to OpenClaw's built-in agents when PACS has no match.
165
+ * @param {string} message
166
+ * @returns {Promise<object>}
167
+ */
168
+ export async function fallbackToOpenClawAgents(message) {
169
+ if (!_openClawAgents) {
170
+ return {
171
+ response: 'Kein OpenClaw-Agent verfügbar (Bridge nicht mit OpenClaw-API verbunden).',
172
+ routedTo: null,
173
+ fallback: true,
174
+ };
175
+ }
176
+
177
+ try {
178
+ // Delegate to OpenClaw's main agent dispatcher
179
+ const result = await _openClawAgents.dispatch(message);
180
+ return {
181
+ response: result.response || result,
182
+ routedTo: result.agent || 'openclaw-default',
183
+ fallback: true,
184
+ };
185
+ } catch (err) {
186
+ return {
187
+ response: `OpenClaw-Fallback fehlgeschlagen: ${err.message}`,
188
+ routedTo: null,
189
+ fallback: true,
190
+ };
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Handle AGENT subcommands (list, add, remove).
196
+ * @param {string} message
197
+ * @returns {object}
198
+ */
199
+ function handleAgentCommand(message) {
200
+ const upper = message.trim().toUpperCase();
201
+
202
+ if (upper === 'AGENT LIST' || upper === 'AGENT LISTE') {
203
+ const agents = listAgents();
204
+ if (agents.length === 0) {
205
+ return { response: 'Keine Agenten registriert.', routedTo: 'PACS-registry', fallback: false };
206
+ }
207
+ const lines = agents.map(
208
+ (a) => `• ${a.name} (${a.domain}) — ${a.status}`
209
+ );
210
+ return {
211
+ response: `Agenten:\n${lines.join('\n')}`,
212
+ routedTo: 'PACS-registry',
213
+ fallback: false,
214
+ };
215
+ }
216
+
217
+ // AGENT ADD <name> ...
218
+ const addMatch = message.match(/^AGENT\s+ADD\s+(\w+)\s*(.*)/i);
219
+ if (addMatch) {
220
+ const [, name, rest] = addMatch;
221
+ const agent = registerAgent(name, { domain: 'custom', description: rest.trim() });
222
+ return {
223
+ response: `✓ Agent "${name}" registriert.`,
224
+ routedTo: 'PACS-registry',
225
+ fallback: false,
226
+ };
227
+ }
228
+
229
+ // AGENT REMOVE <name>
230
+ const removeMatch = message.match(/^AGENT\s+REMOVE\s+(\w+)/i);
231
+ if (removeMatch) {
232
+ const { removeAgent } = require('./agent-registry.js');
233
+ const removed = removeAgent(removeMatch[1]);
234
+ return {
235
+ response: removed
236
+ ? `✓ Agent "${removeMatch[1]}" entfernt.`
237
+ : `Agent "${removeMatch[1]}" nicht gefunden.`,
238
+ routedTo: 'PACS-registry',
239
+ fallback: false,
240
+ };
241
+ }
242
+
243
+ return {
244
+ response: 'Unbekannter AGENT-Befehl. Verfügbar: AGENT LIST, AGENT ADD <name>, AGENT REMOVE <name>',
245
+ routedTo: 'PACS-registry',
246
+ fallback: false,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Handle learn-mode subcommands.
252
+ * @param {string} message
253
+ * @returns {object}
254
+ */
255
+ function handleLearnModeCommand(message) {
256
+ const upper = message.trim().toUpperCase();
257
+
258
+ if (upper.includes('GET') || upper === 'LERNE MODUS') {
259
+ const mode = LearnMode.getLearnMode();
260
+ return {
261
+ response: `Learn-Mode: ${mode}`,
262
+ routedTo: 'PACS-learn-mode',
263
+ fallback: false,
264
+ };
265
+ }
266
+
267
+ const setMatch = message.match(/SET\s+(SAFE|EXPLICIT|AUTO)/i);
268
+ if (setMatch) {
269
+ const newMode = setMatch[1].toLowerCase();
270
+ LearnMode.setLearnMode(newMode);
271
+ return {
272
+ response: `✓ Learn-Mode gesetzt auf: ${newMode}`,
273
+ routedTo: 'PACS-learn-mode',
274
+ fallback: false,
275
+ };
276
+ }
277
+
278
+ return {
279
+ response: 'Unbekannter LERNE-Befehl. Nutze: LERNE MODUS GET oder LERNE MODUS SET <safe|explicit|auto>',
280
+ routedTo: 'PACS-learn-mode',
281
+ fallback: false,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Get current bridge status.
287
+ * @returns {object}
288
+ */
289
+ export function status() {
290
+ return {
291
+ initialized: _openClawConfig !== null,
292
+ mode: _openClawConfig?.pacs?.mode || 'integrated',
293
+ enabled: _openClawConfig?.pacs?.enabled !== false,
294
+ openClawConnected: _openClawAgents !== null,
295
+ };
296
+ }
297
+
298
+ export default {
299
+ init,
300
+ query,
301
+ isPACSQuery,
302
+ fallbackToOpenClawAgents,
303
+ status,
304
+ };
@@ -0,0 +1,221 @@
1
+ /**
2
+ * PACS Core — Routing Engine
3
+ * Routes incoming user tasks to the correct agent(s) with scoring
4
+ */
5
+
6
+ import crypto from './crypto.js';
7
+ import memoryStore from './memory-store.js';
8
+ import agentRegistry from './agent-registry.js';
9
+
10
+ // Domain keyword maps for zero-config routing
11
+ const DOMAIN_KEYWORDS = {
12
+ finance: ['finanzen', 'geld', 'budget', 'kosten', 'einnahmen', 'ausgaben', 'steuer', 'bank', 'investment', 'aktien', 'buchhaltung', 'rechnung', 'lohn', 'gehalt', 'finance', 'money', 'budget', 'cost', 'income', 'expense', 'tax', 'investment', 'stock', 'accounting', 'invoice', 'salary', ' payment'],
13
+ code: ['code', 'programm', 'skript', 'entwickle', 'debug', 'api', 'frontend', 'backend', 'database', 'funktion', 'software', 'github', 'git', 'deploy', 'build', 'compile', 'node', 'javascript', 'python', 'rust', 'java', 'c++', 'sql', 'html', 'css', 'react', 'nextjs', 'vite', 'typescript'],
14
+ social: ['social', 'instagram', 'facebook', 'twitter', 'linkedin', 'youtube', 'tiktok', 'post', 'posting', 'content', 'marketing', 'kampagne', 'werbung', 'community', '粉丝', '粉丝', 'engagement', 'follower', ' viral'],
15
+ assistant: ['hilf', 'help', 'organisiere', 'organize', 'plane', 'plan', 'todo', 'aufgabe', 'task', 'meeting', 'kalender', 'calendar', 'email', 'termin', 'reminder', 'erinnerung', 'todoist', 'notion'],
16
+ research: ['recherche', 'research', 'suche', 'search', 'analyse', 'analysis', 'studie', 'study', 'wettbewerb', 'competition', 'benchmark', 'vergleich', 'compare', 'trends', 'markt', 'market'],
17
+ health: ['gesundheit', 'health', 'fitness', 'sport', 'ernährung', 'diet', 'arbeit', 'work', 'stress', ' burnout', 'schlaf', 'sleep', 'meditation', 'yoga', 'arzt', 'doctor'],
18
+ home: ['smart home', 'haus', 'home', 'iot', 'automation', 'heizung', 'heating', 'licht', 'light', 'kamera', 'camera', 'tür', 'door', 'alarmanlage', 'alarm'],
19
+ travel: ['reise', 'travel', 'urlaub', 'vacation', 'flug', 'flight', 'hotel', 'booking', 'airbnb', 'restaurant', 'trip', 'planung', 'planning'],
20
+ };
21
+
22
+ const AGENT_ALIASES = {
23
+ finance: ['finance', 'finance-agent', 'buchhaltung', 'accounting', 'finanzen-agent'],
24
+ code: ['code', 'code-agent', 'coder', 'developer', 'dev-agent', 'entwickler'],
25
+ social: ['social', 'social-agent', 'marketing', 'content', 'socialmedia', 'social-media'],
26
+ assistant: ['assistant', 'assistent', 'haushalt', 'organizer', 'todo', 'tasks'],
27
+ research: ['research', 'researcher', 'analyse', 'analyst', 'recherche'],
28
+ health: ['health', 'fitness', 'gesundheit', 'wellness'],
29
+ home: ['home', 'smarthome', 'iot', 'automation'],
30
+ travel: ['travel', 'reise', 'trip', 'planner'],
31
+ };
32
+
33
+ // All-agent trigger patterns
34
+ const ALL_AGENTS_PATTERNS = [
35
+ /alle\s+agenten/i,
36
+ /all[ei]\s+agents/i,
37
+ /every\s+agent/i,
38
+ /alle\s*i/i,
39
+ /^alle$/i,
40
+ /^all$/i,
41
+ ];
42
+
43
+ /**
44
+ * Score how well an agent matches a user message.
45
+ * @param {object} agent - Agent definition
46
+ * @param {string} message - User message
47
+ * @param {string[]} routingHints - Loaded routing hints
48
+ * @returns {number} Score 0-1
49
+ */
50
+ function scoreAgent(agent, message, routingHints = []) {
51
+ const msg = message.toLowerCase();
52
+ let score = 0;
53
+ const domain = (agent.domain || '').toLowerCase();
54
+ const capabilities = (agent.capabilities || []).map((c) => c.toLowerCase());
55
+ const description = (agent.description || '').toLowerCase();
56
+
57
+ // 1. Check routing hints (highest weight)
58
+ for (const hint of routingHints) {
59
+ const hintLower = hint.toLowerCase();
60
+ // Hint format: "finance → finance-agent" or "finance" alone
61
+ const [hintTopic, hintTarget] = hintLower.split('→').map((s) => s.trim());
62
+ if (hintTarget && hintTarget.includes(agent.name.toLowerCase())) {
63
+ if (msg.includes(hintTopic)) {
64
+ score += 0.5;
65
+ }
66
+ } else if (hintLower.includes(agent.name.toLowerCase())) {
67
+ // Direct name mention in hint
68
+ score += 0.3;
69
+ }
70
+ }
71
+
72
+ // 2. Domain keyword match
73
+ const domainTerms = DOMAIN_KEYWORDS[domain] || [];
74
+ for (const term of domainTerms) {
75
+ if (msg.includes(term)) {
76
+ score += 0.15;
77
+ }
78
+ }
79
+
80
+ // 3. Capability match
81
+ for (const cap of capabilities) {
82
+ if (msg.includes(cap)) {
83
+ score += 0.1;
84
+ }
85
+ }
86
+
87
+ // 4. Description match
88
+ for (const term of msg.split(/\s+/)) {
89
+ if (term.length > 3 && description.includes(term)) {
90
+ score += 0.05;
91
+ }
92
+ }
93
+
94
+ // 5. Exact name/alias mention in message
95
+ const agentTerms = [agent.name, ...(AGENT_ALIASES[domain] || [])];
96
+ for (const term of agentTerms) {
97
+ if (msg.includes(term.toLowerCase())) {
98
+ score += 0.2;
99
+ }
100
+ }
101
+
102
+ return Math.min(score, 1.0);
103
+ }
104
+
105
+ /**
106
+ * Route a user message to the best matching agents.
107
+ * @param {string} userMessage - The user's input
108
+ * @returns {object} { task, plan: [{agent, mode, score}], confidence }
109
+ */
110
+ export function route(userMessage) {
111
+ if (!userMessage || typeof userMessage !== 'string') {
112
+ return { task: userMessage || '', plan: [], confidence: 0 };
113
+ }
114
+
115
+ const routing = memoryStore.readRouting();
116
+ const hints = routing.hints || [];
117
+ const agents = agentRegistry.listAgents({ status: 'active' });
118
+
119
+ // Handle "all agents" query
120
+ if (ALL_AGENTS_PATTERNS.some((p) => p.test(userMessage))) {
121
+ return {
122
+ task: userMessage,
123
+ plan: agents.map((a) => ({ agent: a.name, mode: 'parallel', score: 1 })),
124
+ confidence: 1,
125
+ };
126
+ }
127
+
128
+ // Handle "who handles X" query
129
+ const whoMatches = userMessage.match(/wer\s+ist\s+(?:für|zuständig|der|die)\s+(.+?)\??$/i);
130
+ if (whoMatches) {
131
+ const topic = whoMatches[1].trim();
132
+ const scores = agents.map((a) => ({
133
+ agent: a.name,
134
+ score: scoreAgent(a, topic, hints),
135
+ }));
136
+ scores.sort((a, b) => b.score - a.score);
137
+ const best = scores[0];
138
+ if (best && best.score > 0) {
139
+ return {
140
+ task: userMessage,
141
+ plan: [{ agent: best.agent, mode: 'single', score: best.score }],
142
+ confidence: best.score,
143
+ };
144
+ }
145
+ }
146
+
147
+ // Score all agents
148
+ const scored = agents.map((a) => ({
149
+ agent: a.name,
150
+ score: scoreAgent(a, userMessage, hints),
151
+ }));
152
+
153
+ // Sort by score descending
154
+ scored.sort((a, b) => b.score - a.score);
155
+
156
+ // Filter to relevant agents (score > 0.05)
157
+ const relevant = scored.filter((s) => s.score > 0.05);
158
+
159
+ if (relevant.length === 0) {
160
+ return { task: userMessage, plan: [], confidence: 0 };
161
+ }
162
+
163
+ const topScore = relevant[0].score;
164
+ const confidence = topScore;
165
+
166
+ return {
167
+ task: userMessage,
168
+ plan: relevant.map((s) => ({ agent: s.agent, mode: 'parallel', score: s.score })),
169
+ confidence,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Route for parallel execution — agents that can work simultaneously.
175
+ * @param {string} userMessage
176
+ * @returns {object} { task, plan: [{agent, mode}], confidence }
177
+ */
178
+ export function routeParallel(userMessage) {
179
+ const result = route(userMessage);
180
+
181
+ // Filter to agents with score >= 0.3 for parallel
182
+ const parallelPlan = result.plan.filter((p) => p.score >= 0.3);
183
+
184
+ return {
185
+ task: result.task,
186
+ plan: parallelPlan.length > 0
187
+ ? parallelPlan.map((p) => ({ agent: p.agent, mode: 'parallel' }))
188
+ : result.plan.slice(0, 1).map((p) => ({ agent: p.agent, mode: 'parallel' })),
189
+ confidence: result.confidence,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Route for sequential execution — agents that must go in order.
195
+ * @param {string} userMessage
196
+ * @returns {object} { task, plan: [{agent, mode}], confidence }
197
+ */
198
+ export function routeSequential(userMessage) {
199
+ const result = route(userMessage);
200
+
201
+ // For sequential, take top 3 agents ordered by score
202
+ const sequentialPlan = result.plan.slice(0, 3);
203
+
204
+ return {
205
+ task: result.task,
206
+ plan: sequentialPlan.map((p) => ({ agent: p.agent, mode: 'sequential' })),
207
+ confidence: result.confidence,
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Get the responsible agent(s) for a specific topic.
213
+ * @param {string} topic
214
+ * @returns {string[]} Agent names
215
+ */
216
+ export function whoHandles(topic) {
217
+ const result = route(topic);
218
+ return result.plan.filter((p) => p.score > 0.1).map((p) => p.agent);
219
+ }
220
+
221
+ export default { route, routeParallel, routeSequential, whoHandles };