twinclaw 1.3.0 → 1.3.1
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/agents/self-setup-agent.js +143 -217
- package/dist/core/gateway.js +31 -11
- package/dist/services/model-router.js +47 -9
- package/package.json +1 -1
|
@@ -1,54 +1,41 @@
|
|
|
1
|
-
var _a;
|
|
2
1
|
import { logThought } from '../utils/logger.js';
|
|
3
2
|
import { checkFirstInteraction, loadUserMemory, saveUserMemory, getDefaultUserMemory, } from '../services/user-memory.js';
|
|
3
|
+
const ONBOARDING_SYSTEM_PROMPT = `You are having a natural conversation to get to know the user better. Your goal is to learn about them in a friendly, casual way - like two people chatting over coffee.
|
|
4
|
+
|
|
5
|
+
Topics to naturally explore (one at a time, naturally):
|
|
6
|
+
- What they do for work/study
|
|
7
|
+
- What they're currently working on or interested in
|
|
8
|
+
- What tools/tech they use
|
|
9
|
+
- What they like to do outside of work
|
|
10
|
+
- How they prefer to communicate (brief vs detailed, technical vs simple)
|
|
11
|
+
- What their goals are
|
|
12
|
+
|
|
13
|
+
Guidelines:
|
|
14
|
+
- Be conversational and warm, not robotic
|
|
15
|
+
- Ask one question at a time, building on what they share
|
|
16
|
+
- Don't use numbered lists or formal questionnaires
|
|
17
|
+
- If they share multiple things, acknowledge them naturally before asking follow-ups
|
|
18
|
+
- When you feel you've learned enough (after 3-5 exchanges), ask if they'd like you to remember this info
|
|
19
|
+
|
|
20
|
+
Important: Extract key information from their responses and include it in your response as a JSON comment like:
|
|
21
|
+
<!-- INFO: {"role": "developer", "interests": "AI, music", "goals": "building automation"} -->
|
|
22
|
+
|
|
23
|
+
This JSON will be automatically parsed to save their profile. Use these keys:
|
|
24
|
+
- role (what they do)
|
|
25
|
+
- company (where they work)
|
|
26
|
+
- currentProject (what they're working on)
|
|
27
|
+
- tools (comma-separated list of tools they use)
|
|
28
|
+
- interests (what they like doing)
|
|
29
|
+
- communicationPreference (brief, detailed, or contextual)
|
|
30
|
+
- frustrations (what frustrates them about AI)
|
|
31
|
+
- goals (what they're trying to achieve)
|
|
32
|
+
- technicalLevel (beginner, intermediate, or expert)`;
|
|
4
33
|
export class SelfSetupAgent {
|
|
5
34
|
#userId;
|
|
6
|
-
#phase = '
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
static DISCOVERY_QUESTIONS = [
|
|
11
|
-
{
|
|
12
|
-
category: 'professional',
|
|
13
|
-
question: "What do you do? (Role, company, industry)",
|
|
14
|
-
field: 'role',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
category: 'professional',
|
|
18
|
-
question: "What are you working on right now? (Current projects)",
|
|
19
|
-
field: 'currentProject',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
category: 'professional',
|
|
23
|
-
question: "What tools do you use daily? (Tech stack, platforms)",
|
|
24
|
-
field: 'tools',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
category: 'personal',
|
|
28
|
-
question: "What do you like to do outside of work?",
|
|
29
|
-
field: 'interests',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
category: 'communication',
|
|
33
|
-
question: "How do you like information presented? (Brief vs detailed, casual vs formal)",
|
|
34
|
-
field: 'communicationPreference',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
category: 'communication',
|
|
38
|
-
question: "What frustrates you about AI assistants?",
|
|
39
|
-
field: 'frustrations',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
category: 'goals',
|
|
43
|
-
question: "What are you trying to achieve? (Short and long term)",
|
|
44
|
-
field: 'goals',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
category: 'technical',
|
|
48
|
-
question: "How technical should I be with you?",
|
|
49
|
-
field: 'technicalLevel',
|
|
50
|
-
},
|
|
51
|
-
];
|
|
35
|
+
#phase = 'idle';
|
|
36
|
+
#conversationTurn = 0;
|
|
37
|
+
#extractedInfo = {};
|
|
38
|
+
#confirmed = false;
|
|
52
39
|
constructor(userId) {
|
|
53
40
|
this.#userId = userId;
|
|
54
41
|
}
|
|
@@ -61,215 +48,147 @@ export class SelfSetupAgent {
|
|
|
61
48
|
isComplete() {
|
|
62
49
|
return this.#phase === 'completed';
|
|
63
50
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
51
|
+
getSystemPrompt() {
|
|
52
|
+
return ONBOARDING_SYSTEM_PROMPT;
|
|
53
|
+
}
|
|
54
|
+
getConversationHistory() {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
role: 'system',
|
|
58
|
+
content: ONBOARDING_SYSTEM_PROMPT,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
processLLMResponse(llmText) {
|
|
63
|
+
this.#conversationTurn++;
|
|
64
|
+
const extracted = this.#extractInfo(llmText);
|
|
65
|
+
if (extracted) {
|
|
66
|
+
this.#extractedInfo = { ...this.#extractedInfo, ...extracted };
|
|
67
|
+
}
|
|
68
|
+
if (this.#conversationTurn >= 4 && !this.#confirmed) {
|
|
69
|
+
this.#phase = 'confirming';
|
|
79
70
|
return {
|
|
80
|
-
text:
|
|
71
|
+
text: llmText,
|
|
81
72
|
phase: this.#phase,
|
|
82
|
-
isComplete:
|
|
83
|
-
needsMoreInfo:
|
|
73
|
+
isComplete: false,
|
|
74
|
+
needsMoreInfo: true,
|
|
75
|
+
extractedInfo: this.#extractedInfo,
|
|
84
76
|
};
|
|
85
77
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
phase: this.#phase,
|
|
89
|
-
isComplete: false,
|
|
90
|
-
needsMoreInfo: true,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
#startWarmIntro() {
|
|
94
|
-
this.#phase = 'warm_intro';
|
|
95
|
-
logThought(`[SelfSetupAgent] Starting warm intro for user: ${this.#userId}`);
|
|
96
|
-
return {
|
|
97
|
-
text: `Hey! I'm noticing this is our first conversation. Before we dive into anything, I'd love to take a few minutes to get to know you better so I can be actually helpful (not just generic AI helpful).
|
|
98
|
-
|
|
99
|
-
Think of this as a quick coffee chat where I learn what makes you tick. Cool?
|
|
100
|
-
|
|
101
|
-
To start - what do you do, and what kind of thing are you looking to build?`,
|
|
102
|
-
phase: this.#phase,
|
|
103
|
-
isComplete: false,
|
|
104
|
-
needsMoreInfo: true,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
#handleWarmIntroResponse(userMessage) {
|
|
108
|
-
this.#discoveryAnswers.role = userMessage;
|
|
109
|
-
this.#phase = 'discovery';
|
|
110
|
-
this.#discoveryQuestionIndex = 0;
|
|
111
|
-
return this.#askNextDiscoveryQuestion();
|
|
112
|
-
}
|
|
113
|
-
#askNextDiscoveryQuestion() {
|
|
114
|
-
const question = _a.DISCOVERY_QUESTIONS[this.#discoveryQuestionIndex];
|
|
115
|
-
if (!question) {
|
|
116
|
-
return this.#moveToSynthesis();
|
|
78
|
+
if (this.#confirmed) {
|
|
79
|
+
return this.#completeSetup();
|
|
117
80
|
}
|
|
118
|
-
|
|
81
|
+
this.#phase = 'learning';
|
|
119
82
|
return {
|
|
120
|
-
text:
|
|
83
|
+
text: llmText,
|
|
121
84
|
phase: this.#phase,
|
|
122
85
|
isComplete: false,
|
|
123
86
|
needsMoreInfo: true,
|
|
87
|
+
extractedInfo: this.#extractedInfo,
|
|
124
88
|
};
|
|
125
89
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const field = question.field;
|
|
130
|
-
this.#discoveryAnswers[field] = userMessage;
|
|
131
|
-
}
|
|
132
|
-
this.#discoveryQuestionIndex++;
|
|
133
|
-
if (this.#discoveryQuestionIndex >= _a.DISCOVERY_QUESTIONS.length) {
|
|
134
|
-
return this.#moveToSynthesis();
|
|
135
|
-
}
|
|
136
|
-
return this.#askNextDiscoveryQuestion();
|
|
90
|
+
confirmAndSave() {
|
|
91
|
+
this.#confirmed = true;
|
|
92
|
+
return this.#completeSetup();
|
|
137
93
|
}
|
|
138
|
-
|
|
139
|
-
this.#phase = '
|
|
140
|
-
const answers = this.#discoveryAnswers;
|
|
141
|
-
const synthesis = this.#buildSynthesis(answers);
|
|
94
|
+
skipOrDecline() {
|
|
95
|
+
this.#phase = 'completed';
|
|
142
96
|
return {
|
|
143
|
-
text:
|
|
144
|
-
|
|
145
|
-
Did I get that right? Anything I should adjust or add?`,
|
|
97
|
+
text: "No problem! Whenever you're ready to share more about yourself, just let me know. How can I help you today?",
|
|
146
98
|
phase: this.#phase,
|
|
147
|
-
isComplete:
|
|
148
|
-
needsMoreInfo:
|
|
99
|
+
isComplete: true,
|
|
100
|
+
needsMoreInfo: false,
|
|
149
101
|
};
|
|
150
102
|
}
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (answers.currentProject) {
|
|
157
|
-
insights.push(`currently focused on ${answers.currentProject}`);
|
|
158
|
-
}
|
|
159
|
-
if (answers.tools && answers.tools.length > 0) {
|
|
160
|
-
insights.push(`using tools like ${answers.tools}`);
|
|
161
|
-
}
|
|
162
|
-
if (answers.interests) {
|
|
163
|
-
insights.push(`into ${answers.interests} outside work`);
|
|
164
|
-
}
|
|
165
|
-
let preference = 'contextual';
|
|
166
|
-
if (answers.communicationPreference) {
|
|
167
|
-
if (answers.communicationPreference.includes('brief')) {
|
|
168
|
-
preference = 'brief';
|
|
103
|
+
#completeSetup() {
|
|
104
|
+
if (Object.keys(this.#extractedInfo).length > 0) {
|
|
105
|
+
const memory = getDefaultUserMemory();
|
|
106
|
+
if (this.#extractedInfo.role) {
|
|
107
|
+
memory.profile.identity.role = String(this.#extractedInfo.role);
|
|
169
108
|
}
|
|
170
|
-
|
|
171
|
-
|
|
109
|
+
if (this.#extractedInfo.company) {
|
|
110
|
+
memory.profile.identity.company = String(this.#extractedInfo.company);
|
|
172
111
|
}
|
|
112
|
+
if (this.#extractedInfo.currentProject) {
|
|
113
|
+
memory.profile.professionalBackground.currentFocus = String(this.#extractedInfo.currentProject);
|
|
114
|
+
}
|
|
115
|
+
if (this.#extractedInfo.tools) {
|
|
116
|
+
const tools = String(this.#extractedInfo.tools).split(',').map(t => t.trim()).filter(Boolean);
|
|
117
|
+
memory.profile.professionalBackground.toolsAndPlatforms = tools;
|
|
118
|
+
}
|
|
119
|
+
if (this.#extractedInfo.interests) {
|
|
120
|
+
memory.profile.personalContext.interests = String(this.#extractedInfo.interests).split(',').map(t => t.trim()).filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
if (this.#extractedInfo.goals) {
|
|
123
|
+
memory.profile.goals.shortTerm = [String(this.#extractedInfo.goals)];
|
|
124
|
+
}
|
|
125
|
+
if (this.#extractedInfo.technicalLevel) {
|
|
126
|
+
const level = String(this.#extractedInfo.technicalLevel).toLowerCase();
|
|
127
|
+
if (level.includes('beginner')) {
|
|
128
|
+
memory.profile.identity.technicalLevel = 'beginner';
|
|
129
|
+
}
|
|
130
|
+
else if (level.includes('expert') || level.includes('advanced')) {
|
|
131
|
+
memory.profile.identity.technicalLevel = 'expert';
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
memory.profile.identity.technicalLevel = 'intermediate';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (this.#extractedInfo.communicationPreference) {
|
|
138
|
+
const pref = String(this.#extractedInfo.communicationPreference).toLowerCase();
|
|
139
|
+
if (pref.includes('brief')) {
|
|
140
|
+
memory.profile.communicationStyle.detailLevel = 'brief';
|
|
141
|
+
}
|
|
142
|
+
else if (pref.includes('detail')) {
|
|
143
|
+
memory.profile.communicationStyle.detailLevel = 'comprehensive';
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
memory.profile.communicationStyle.detailLevel = 'contextual';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (this.#extractedInfo.frustrations) {
|
|
150
|
+
memory.profile.communicationStyle.frustrations = [String(this.#extractedInfo.frustrations)];
|
|
151
|
+
}
|
|
152
|
+
saveUserMemory(this.#userId, memory);
|
|
153
|
+
logThought(`[SelfSetupAgent] Saved user profile for: ${this.#userId}`);
|
|
173
154
|
}
|
|
174
|
-
insights.push(`prefer ${preference} communication`);
|
|
175
|
-
if (answers.goals) {
|
|
176
|
-
insights.push(`your goals include ${answers.goals}`);
|
|
177
|
-
}
|
|
178
|
-
const synthesis = insights.join('. ');
|
|
179
|
-
return `Alright, here's what I'm picking up:\n- ${insights.join('\n- ')}`;
|
|
180
|
-
}
|
|
181
|
-
#handleSynthesisResponse(userMessage) {
|
|
182
|
-
const lowerResponse = userMessage.toLowerCase();
|
|
183
|
-
if (lowerResponse.includes('yes') || lowerResponse.includes('right') || lowerResponse.includes('correct') || lowerResponse.includes('yep')) {
|
|
184
|
-
return this.#completeSetup();
|
|
185
|
-
}
|
|
186
|
-
return this.#completeSetup();
|
|
187
|
-
}
|
|
188
|
-
#completeSetup() {
|
|
189
|
-
this.#phase = 'file_creation';
|
|
190
|
-
const memory = this.#buildUserMemory();
|
|
191
|
-
saveUserMemory(this.#userId, memory);
|
|
192
155
|
this.#phase = 'completed';
|
|
193
|
-
logThought(`[SelfSetupAgent] Completed setup for user: ${this.#userId}`);
|
|
194
156
|
return {
|
|
195
|
-
text: `
|
|
196
|
-
- Your profile & preferences
|
|
197
|
-
- How you like me to communicate
|
|
198
|
-
- What you're currently working on
|
|
199
|
-
- Quick facts I can reference
|
|
157
|
+
text: `Got it! I've saved what you shared about yourself. I'll use this to tailor my responses to you.
|
|
200
158
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
✓ Remember our conversations and what matters to you
|
|
205
|
-
✓ Adapt my style to what works for you
|
|
159
|
+
From now on, I'll:
|
|
160
|
+
✓ Remember what matters to you
|
|
161
|
+
✓ Adapt my communication style to your preferences
|
|
206
162
|
✓ Keep track of your projects and goals
|
|
207
|
-
✓ Update my notes as things change
|
|
208
163
|
|
|
209
|
-
If
|
|
164
|
+
If anything changes or you want to update your info, just let me know. Ready to dive in!`,
|
|
210
165
|
phase: this.#phase,
|
|
211
166
|
isComplete: true,
|
|
212
167
|
needsMoreInfo: false,
|
|
168
|
+
extractedInfo: this.#extractedInfo,
|
|
213
169
|
};
|
|
214
170
|
}
|
|
215
|
-
#
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
defaultMemory.profile.identity.technicalLevel = this.#mapTechnicalLevel(answers.technicalLevel);
|
|
220
|
-
defaultMemory.profile.professionalBackground.currentFocus = answers.currentProject;
|
|
221
|
-
if (answers.tools) {
|
|
222
|
-
defaultMemory.profile.professionalBackground.toolsAndPlatforms = Array.isArray(answers.tools)
|
|
223
|
-
? answers.tools
|
|
224
|
-
: [answers.tools];
|
|
225
|
-
}
|
|
226
|
-
defaultMemory.profile.personalContext.interests = answers.interests ? [answers.interests] : [];
|
|
227
|
-
if (answers.communicationPreference) {
|
|
228
|
-
defaultMemory.profile.communicationStyle.detailLevel = this.#mapDetailLevel(answers.communicationPreference);
|
|
171
|
+
#extractInfo(text) {
|
|
172
|
+
const infoMatch = text.match(/<!--\s*INFO:\s*(\{[^}]+\})\s*-->/);
|
|
173
|
+
if (!infoMatch) {
|
|
174
|
+
return null;
|
|
229
175
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
? answers.frustrations
|
|
233
|
-
: [answers.frustrations];
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(infoMatch[1]);
|
|
234
178
|
}
|
|
235
|
-
|
|
236
|
-
|
|
179
|
+
catch {
|
|
180
|
+
logThought(`[SelfSetupAgent] Failed to parse extracted info: ${infoMatch[1]}`);
|
|
181
|
+
return null;
|
|
237
182
|
}
|
|
238
|
-
return defaultMemory;
|
|
239
|
-
}
|
|
240
|
-
#mapTechnicalLevel(level) {
|
|
241
|
-
if (!level)
|
|
242
|
-
return undefined;
|
|
243
|
-
const lower = level.toLowerCase();
|
|
244
|
-
if (lower.includes('expert') || lower.includes('advanced'))
|
|
245
|
-
return 'expert';
|
|
246
|
-
if (lower.includes('beginner') || lower.includes('new'))
|
|
247
|
-
return 'beginner';
|
|
248
|
-
return 'intermediate';
|
|
249
|
-
}
|
|
250
|
-
#mapDetailLevel(pref) {
|
|
251
|
-
if (!pref)
|
|
252
|
-
return undefined;
|
|
253
|
-
const lower = pref.toLowerCase();
|
|
254
|
-
if (lower.includes('brief') || lower.includes('short'))
|
|
255
|
-
return 'brief';
|
|
256
|
-
if (lower.includes('detail') || lower.includes('comprehensive'))
|
|
257
|
-
return 'comprehensive';
|
|
258
|
-
return 'contextual';
|
|
259
|
-
}
|
|
260
|
-
cancel() {
|
|
261
|
-
this.#phase = 'cancelled';
|
|
262
|
-
logThought(`[SelfSetupAgent] Setup cancelled for user: ${this.#userId}`);
|
|
263
183
|
}
|
|
264
184
|
getState() {
|
|
265
185
|
return {
|
|
266
186
|
phase: this.#phase,
|
|
267
|
-
|
|
268
|
-
|
|
187
|
+
turn: this.#conversationTurn,
|
|
188
|
+
extractedCount: Object.keys(this.#extractedInfo).length,
|
|
269
189
|
};
|
|
270
190
|
}
|
|
271
191
|
}
|
|
272
|
-
_a = SelfSetupAgent;
|
|
273
192
|
export function createSelfSetupAgent(userId) {
|
|
274
193
|
if (!checkFirstInteraction(userId)) {
|
|
275
194
|
return null;
|
|
@@ -279,3 +198,10 @@ export function createSelfSetupAgent(userId) {
|
|
|
279
198
|
export function getExistingUserMemory(userId) {
|
|
280
199
|
return loadUserMemory(userId);
|
|
281
200
|
}
|
|
201
|
+
export function getOrCreateAgent(userId, existing) {
|
|
202
|
+
if (existing && existing.phase !== 'completed') {
|
|
203
|
+
return existing;
|
|
204
|
+
}
|
|
205
|
+
const newAgent = createSelfSetupAgent(userId);
|
|
206
|
+
return newAgent ?? existing;
|
|
207
|
+
}
|
package/dist/core/gateway.js
CHANGED
|
@@ -170,24 +170,44 @@ export class Gateway {
|
|
|
170
170
|
const sessionId = `${message.platform}:${message.senderId}`;
|
|
171
171
|
const userId = message.senderId;
|
|
172
172
|
const existingAgent = this.#selfSetupAgents.get(userId);
|
|
173
|
-
if (existingAgent) {
|
|
174
|
-
|
|
175
|
-
if (response.isComplete) {
|
|
176
|
-
this.#selfSetupAgents.delete(userId);
|
|
177
|
-
if (response.isComplete && !response.needsMoreInfo) {
|
|
178
|
-
return response.text;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return response.text;
|
|
173
|
+
if (existingAgent && !existingAgent.isComplete()) {
|
|
174
|
+
return this.#runOnboardingConversation(existingAgent, normalizedText, sessionId);
|
|
182
175
|
}
|
|
183
176
|
const newAgent = createSelfSetupAgent(userId);
|
|
184
177
|
if (newAgent) {
|
|
185
178
|
this.#selfSetupAgents.set(userId, newAgent);
|
|
186
|
-
|
|
187
|
-
return response.text;
|
|
179
|
+
return this.#runOnboardingConversation(newAgent, normalizedText, sessionId);
|
|
188
180
|
}
|
|
189
181
|
return this.processText(sessionId, normalizedText);
|
|
190
182
|
}
|
|
183
|
+
async #runOnboardingConversation(agent, userMessage, sessionId) {
|
|
184
|
+
const messages = [
|
|
185
|
+
{ role: 'system', content: agent.getSystemPrompt() },
|
|
186
|
+
...agent.getConversationHistory(),
|
|
187
|
+
{ role: 'user', content: userMessage },
|
|
188
|
+
];
|
|
189
|
+
try {
|
|
190
|
+
const response = await this.#router.createChatCompletion(messages, undefined, { sessionId });
|
|
191
|
+
if (!response.content) {
|
|
192
|
+
return "I'm having trouble thinking right now. Let's try again.";
|
|
193
|
+
}
|
|
194
|
+
const result = agent.processLLMResponse(response.content);
|
|
195
|
+
if (result.isComplete && !result.needsMoreInfo) {
|
|
196
|
+
this.#selfSetupAgents.delete(agent.userId);
|
|
197
|
+
return result.text;
|
|
198
|
+
}
|
|
199
|
+
if (result.phase === 'confirming') {
|
|
200
|
+
const confirmMessage = result.text + "\n\nWould you like me to remember this? (yes/no or just tell me more)";
|
|
201
|
+
return confirmMessage;
|
|
202
|
+
}
|
|
203
|
+
return result.text;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
207
|
+
logThought(`[SelfSetupAgent] LLM error: ${errorMsg}`);
|
|
208
|
+
return "I'm having trouble continuing our conversation. Let's try again - what would you like to talk about?";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
191
211
|
async processText(sessionId, text) {
|
|
192
212
|
const normalizedText = text.trim();
|
|
193
213
|
if (!normalizedText) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { MODEL_SLOT_IDS, } from '../types/model-routing.js';
|
|
3
|
-
import { scrubSensitiveText } from '../utils/logger.js';
|
|
3
|
+
import { scrubSensitiveText, logThought } from '../utils/logger.js';
|
|
4
4
|
import { getModelRoutingSetting, listModelRoutingEvents, saveModelRoutingEvent, saveModelRoutingSetting, } from './db-model-routing.js';
|
|
5
5
|
import { getSecretVaultService } from './secret-vault.js';
|
|
6
6
|
import { getGitHubCopilotService } from './github-copilot-service.js';
|
|
@@ -434,11 +434,18 @@ export class ModelRouter {
|
|
|
434
434
|
}
|
|
435
435
|
async getApiKey(config) {
|
|
436
436
|
const envName = config.apiKeyEnvName;
|
|
437
|
+
const providerId = this.resolveProviderId(config);
|
|
437
438
|
const key = getSecretVaultService().readSecret(envName);
|
|
438
439
|
if (!key) {
|
|
439
|
-
|
|
440
|
+
const configKey = getConfigValue(envName);
|
|
441
|
+
if (configKey) {
|
|
442
|
+
logThought(`[Router] Using API key from config for ${providerId}: ${envName.slice(0, 8)}...`);
|
|
443
|
+
return configKey;
|
|
444
|
+
}
|
|
445
|
+
console.warn(`[Router] Warning: API key ${envName} is not set in environment or config (provider: ${providerId}).`);
|
|
440
446
|
return '';
|
|
441
447
|
}
|
|
448
|
+
logThought(`[Router] Using API key from vault for ${providerId}: ${envName.slice(0, 8)}...`);
|
|
442
449
|
if (envName !== 'GITHUB_TOKEN') {
|
|
443
450
|
return key;
|
|
444
451
|
}
|
|
@@ -1009,19 +1016,26 @@ export class ModelRouter {
|
|
|
1009
1016
|
}
|
|
1010
1017
|
loadModelsFromConfig() {
|
|
1011
1018
|
const configModels = [];
|
|
1019
|
+
const definitionsJson = getConfigValue('MODEL_DEFINITIONS');
|
|
1020
|
+
const primaryModel = getConfigValue('PRIMARY_MODEL');
|
|
1021
|
+
logThought(`[Router] loadModelsFromConfig: PRIMARY_MODEL=${primaryModel}, MODEL_DEFINITIONS present=${!!definitionsJson}`);
|
|
1012
1022
|
// First, check if config.models.definitions has any models (from model picker)
|
|
1013
1023
|
// If so, use those instead of hardcoded fallbacks
|
|
1014
|
-
const definitionsJson = getConfigValue('MODEL_DEFINITIONS');
|
|
1015
1024
|
if (definitionsJson) {
|
|
1016
1025
|
try {
|
|
1017
1026
|
const definitions = JSON.parse(definitionsJson);
|
|
1018
1027
|
if (definitions && definitions.length > 0) {
|
|
1019
1028
|
const primaryModel = getConfigValue('PRIMARY_MODEL') ?? '';
|
|
1029
|
+
// Normalize model ID for comparison (modal/zai-glm-5-fp8 -> modal-zai-glm-5-fp8)
|
|
1030
|
+
const normalizeId = (id) => id.replace('/', '-').toLowerCase();
|
|
1031
|
+
const normalizedPrimary = normalizeId(primaryModel);
|
|
1020
1032
|
// Sort models so primary is first, then the rest
|
|
1021
1033
|
const sortedDefinitions = [...definitions].sort((a, b) => {
|
|
1022
|
-
|
|
1034
|
+
const aNorm = normalizeId(a.id);
|
|
1035
|
+
const bNorm = normalizeId(b.id);
|
|
1036
|
+
if (aNorm === normalizedPrimary)
|
|
1023
1037
|
return -1;
|
|
1024
|
-
if (
|
|
1038
|
+
if (bNorm === normalizedPrimary)
|
|
1025
1039
|
return 1;
|
|
1026
1040
|
return 0;
|
|
1027
1041
|
});
|
|
@@ -1033,13 +1047,36 @@ export class ModelRouter {
|
|
|
1033
1047
|
apiKeyEnvName: def.apiKeyEnvName,
|
|
1034
1048
|
});
|
|
1035
1049
|
}
|
|
1036
|
-
//
|
|
1037
|
-
if
|
|
1038
|
-
|
|
1050
|
+
// Even with definitions, add fallback providers in case primary fails
|
|
1051
|
+
// This ensures we have working alternatives if the user's primary model fails
|
|
1052
|
+
const groqApiKey = getConfigValue('GROQ_API_KEY');
|
|
1053
|
+
const openRouterApiKey = getConfigValue('OPENROUTER_API_KEY');
|
|
1054
|
+
const existingIds = new Set(configModels.map(m => m.id));
|
|
1055
|
+
// Add Groq as fallback if not already in definitions
|
|
1056
|
+
if (groqApiKey && !existingIds.has('groq-qwen3-32b') && !existingIds.has(MODEL_SLOT_IDS.FALLBACK_1)) {
|
|
1057
|
+
configModels.push({
|
|
1058
|
+
id: MODEL_SLOT_IDS.FALLBACK_1,
|
|
1059
|
+
model: 'qwen/qwen3-32b',
|
|
1060
|
+
baseURL: 'https://api.groq.com/openai/v1/chat/completions',
|
|
1061
|
+
apiKeyEnvName: 'GROQ_API_KEY',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
// Add OpenRouter free model as secondary fallback
|
|
1065
|
+
if (openRouterApiKey && !existingIds.has('openrouter-arcee-ai-trinity-mini-free') && !existingIds.has(MODEL_SLOT_IDS.FALLBACK_2)) {
|
|
1066
|
+
configModels.push({
|
|
1067
|
+
id: MODEL_SLOT_IDS.FALLBACK_2,
|
|
1068
|
+
model: 'arcee-ai/trinity-mini:free',
|
|
1069
|
+
baseURL: 'https://openrouter.ai/api/v1/chat/completions',
|
|
1070
|
+
apiKeyEnvName: 'OPENROUTER_API_KEY',
|
|
1071
|
+
});
|
|
1039
1072
|
}
|
|
1073
|
+
logThought(`[Router] Loaded ${configModels.length} models from definitions. PRIMARY: ${primaryModel}`);
|
|
1074
|
+
// Return all models including fallbacks
|
|
1075
|
+
return configModels;
|
|
1040
1076
|
}
|
|
1041
1077
|
}
|
|
1042
|
-
catch {
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
logThought(`[Router] Failed to load definitions: ${e}`);
|
|
1043
1080
|
// JSON parse failed, fall through to legacy logic
|
|
1044
1081
|
}
|
|
1045
1082
|
}
|
|
@@ -1158,6 +1195,7 @@ export class ModelRouter {
|
|
|
1158
1195
|
apiKeyEnvName: 'MODAL_API_KEY',
|
|
1159
1196
|
});
|
|
1160
1197
|
}
|
|
1198
|
+
logThought(`[Router] Final loaded models (${configModels.length}): ${configModels.map(m => `${m.id}->${m.model.split('/').pop()}`).join(', ')}`);
|
|
1161
1199
|
return configModels;
|
|
1162
1200
|
}
|
|
1163
1201
|
}
|