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.
- package/LICENSE +21 -0
- package/README.md +602 -0
- package/bin/pacs-cli.js +7 -0
- package/package.json +31 -0
- package/phase2-test.mjs +53 -0
- package/src/agent-creator.js +178 -0
- package/src/agent-registry.js +165 -0
- package/src/bootstrap.js +245 -0
- package/src/cli.js +384 -0
- package/src/crypto.js +118 -0
- package/src/index.js +74 -0
- package/src/inter-agent-bus.js +187 -0
- package/src/learn-mode.js +97 -0
- package/src/learning-loop.js +254 -0
- package/src/memory-store.js +226 -0
- package/src/openclaw-bridge.cjs +214 -0
- package/src/openclaw-bridge.js +304 -0
- package/src/routing-engine.js +221 -0
- package/src/test.js +298 -0
- package/src/trigger-parser.js +187 -0
- package/src/triggers.json +122 -0
|
@@ -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 };
|