@chaaskit/server 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/dist/api/admin.js +438 -0
- package/dist/api/admin.js.map +1 -0
- package/dist/api/agents.js +21 -0
- package/dist/api/agents.js.map +1 -0
- package/dist/api/api-keys.js +122 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/auth.js +399 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/chat.js +900 -0
- package/dist/api/chat.js.map +1 -0
- package/dist/api/config.js +91 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/documents.js +237 -0
- package/dist/api/documents.js.map +1 -0
- package/dist/api/export.js +107 -0
- package/dist/api/export.js.map +1 -0
- package/dist/api/health.js +25 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/mcp-server.js +84 -0
- package/dist/api/mcp-server.js.map +1 -0
- package/dist/api/mcp.js +400 -0
- package/dist/api/mcp.js.map +1 -0
- package/dist/api/mentions.js +94 -0
- package/dist/api/mentions.js.map +1 -0
- package/dist/api/oauth.js +366 -0
- package/dist/api/oauth.js.map +1 -0
- package/dist/api/payments.js +473 -0
- package/dist/api/payments.js.map +1 -0
- package/dist/api/projects.js +301 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/scheduled-prompts.js +617 -0
- package/dist/api/scheduled-prompts.js.map +1 -0
- package/dist/api/search.js +85 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/share.js +188 -0
- package/dist/api/share.js.map +1 -0
- package/dist/api/slack.js +468 -0
- package/dist/api/slack.js.map +1 -0
- package/dist/api/teams.js +693 -0
- package/dist/api/teams.js.map +1 -0
- package/dist/api/templates.js +134 -0
- package/dist/api/templates.js.map +1 -0
- package/dist/api/threads.js +323 -0
- package/dist/api/threads.js.map +1 -0
- package/dist/api/upload.js +57 -0
- package/dist/api/upload.js.map +1 -0
- package/dist/api/user.js +111 -0
- package/dist/api/user.js.map +1 -0
- package/dist/api/v1/openai.js +245 -0
- package/dist/api/v1/openai.js.map +1 -0
- package/dist/app.js +168 -0
- package/dist/app.js.map +1 -0
- package/dist/bin/cli.js +57 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/commands/db-sync.js +108 -0
- package/dist/commands/db-sync.js.map +1 -0
- package/dist/config/loader.js +374 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/documents/extractors.js +136 -0
- package/dist/documents/extractors.js.map +1 -0
- package/dist/extensions/glob.js +53 -0
- package/dist/extensions/glob.js.map +1 -0
- package/dist/extensions/loader.js +72 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/index.js +75 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/mcp/client.js +551 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/server.js +335 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/middleware/apiKeyAuth.js +136 -0
- package/dist/middleware/apiKeyAuth.js.map +1 -0
- package/dist/middleware/auth.js +192 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/errorHandler.js +41 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/mcpServerAuth.js +164 -0
- package/dist/middleware/mcpServerAuth.js.map +1 -0
- package/dist/middleware/requestLogger.js +9 -0
- package/dist/middleware/requestLogger.js.map +1 -0
- package/dist/middleware/team.js +132 -0
- package/dist/middleware/team.js.map +1 -0
- package/dist/oauth/server.js +410 -0
- package/dist/oauth/server.js.map +1 -0
- package/dist/queue/cli.js +93 -0
- package/dist/queue/cli.js.map +1 -0
- package/dist/queue/handlers/index.js +91 -0
- package/dist/queue/handlers/index.js.map +1 -0
- package/dist/queue/handlers/scheduled-prompt.js +270 -0
- package/dist/queue/handlers/scheduled-prompt.js.map +1 -0
- package/dist/queue/index.js +91 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/providers/memory.js +296 -0
- package/dist/queue/providers/memory.js.map +1 -0
- package/dist/queue/providers/sqs.js +275 -0
- package/dist/queue/providers/sqs.js.map +1 -0
- package/dist/queue/scheduler.js +355 -0
- package/dist/queue/scheduler.js.map +1 -0
- package/dist/queue/types.js +5 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/queue/worker.js +230 -0
- package/dist/queue/worker.js.map +1 -0
- package/dist/registry/index.js +40 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/server.js +207 -0
- package/dist/server.js.map +1 -0
- package/dist/services/agent.js +530 -0
- package/dist/services/agent.js.map +1 -0
- package/dist/services/agents.js +194 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/documents.js +507 -0
- package/dist/services/documents.js.map +1 -0
- package/dist/services/email/index.js +91 -0
- package/dist/services/email/index.js.map +1 -0
- package/dist/services/email/providers/ses.js +97 -0
- package/dist/services/email/providers/ses.js.map +1 -0
- package/dist/services/email/templates.js +194 -0
- package/dist/services/email/templates.js.map +1 -0
- package/dist/services/email/types.js +5 -0
- package/dist/services/email/types.js.map +1 -0
- package/dist/services/encryption.js +69 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/oauth-discovery.js +226 -0
- package/dist/services/oauth-discovery.js.map +1 -0
- package/dist/services/pendingConfirmation.js +105 -0
- package/dist/services/pendingConfirmation.js.map +1 -0
- package/dist/services/scheduledPrompts.js +70 -0
- package/dist/services/scheduledPrompts.js.map +1 -0
- package/dist/services/slack/client.js +174 -0
- package/dist/services/slack/client.js.map +1 -0
- package/dist/services/slack/events.js +189 -0
- package/dist/services/slack/events.js.map +1 -0
- package/dist/services/slack/index.js +6 -0
- package/dist/services/slack/index.js.map +1 -0
- package/dist/services/slack/notifications.js +124 -0
- package/dist/services/slack/notifications.js.map +1 -0
- package/dist/services/slack/signature.js +74 -0
- package/dist/services/slack/signature.js.map +1 -0
- package/dist/services/slack/thread-context.js +191 -0
- package/dist/services/slack/thread-context.js.map +1 -0
- package/dist/services/toolConfirmation.js +55 -0
- package/dist/services/toolConfirmation.js.map +1 -0
- package/dist/services/usage.js +241 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/ssr/build.js +90 -0
- package/dist/ssr/build.js.map +1 -0
- package/dist/ssr/components/SSRMessageList.js +120 -0
- package/dist/ssr/components/SSRMessageList.js.map +1 -0
- package/dist/ssr/entry.client.js +8 -0
- package/dist/ssr/entry.client.js.map +1 -0
- package/dist/ssr/entry.server.js +71 -0
- package/dist/ssr/entry.server.js.map +1 -0
- package/dist/ssr/handler.js +51 -0
- package/dist/ssr/handler.js.map +1 -0
- package/dist/ssr/root.js +184 -0
- package/dist/ssr/root.js.map +1 -0
- package/dist/ssr/routes/login.js +140 -0
- package/dist/ssr/routes/login.js.map +1 -0
- package/dist/ssr/routes/pricing.js +195 -0
- package/dist/ssr/routes/pricing.js.map +1 -0
- package/dist/ssr/routes/privacy.js +39 -0
- package/dist/ssr/routes/privacy.js.map +1 -0
- package/dist/ssr/routes/register.js +148 -0
- package/dist/ssr/routes/register.js.map +1 -0
- package/dist/ssr/routes/shared.$shareId.js +153 -0
- package/dist/ssr/routes/shared.$shareId.js.map +1 -0
- package/dist/ssr/routes/terms.js +39 -0
- package/dist/ssr/routes/terms.js.map +1 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/providers/database.js +38 -0
- package/dist/storage/providers/database.js.map +1 -0
- package/dist/storage/providers/filesystem.js +51 -0
- package/dist/storage/providers/filesystem.js.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/tools/documents.js +336 -0
- package/dist/tools/documents.js.map +1 -0
- package/dist/tools/get-plan-usage.js +82 -0
- package/dist/tools/get-plan-usage.js.map +1 -0
- package/dist/tools/index.js +106 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/web-scrape.js +145 -0
- package/dist/tools/web-scrape.js.map +1 -0
- package/package.json +93 -0
package/dist/api/chat.js
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { db } from '@chaaskit/db';
|
|
3
|
+
import { HTTP_STATUS, sendMessageSchema } from '@chaaskit/shared';
|
|
4
|
+
import { optionalAuth, requireAuth, optionalVerifiedEmail } from '../middleware/auth.js';
|
|
5
|
+
import { AppError } from '../middleware/errorHandler.js';
|
|
6
|
+
import { getConfig } from '../config/loader.js';
|
|
7
|
+
import { createAgentService } from '../services/agent.js';
|
|
8
|
+
import { checkUsageLimits, incrementUsage } from '../services/usage.js';
|
|
9
|
+
import { mcpManager } from '../mcp/client.js';
|
|
10
|
+
import { pendingConfirmations } from '../services/pendingConfirmation.js';
|
|
11
|
+
import { checkToolConfirmationRequired, createDenialToolResult } from '../services/toolConfirmation.js';
|
|
12
|
+
import { getAgentById, getDefaultAgent, filterToolsForAgent, toAgentConfig, isBuiltInAgent, getNativeToolsForAgentFiltered } from '../services/agents.js';
|
|
13
|
+
import { executeNativeTool, isNativeTool, resolveNativeToolTemplate } from '../tools/index.js';
|
|
14
|
+
import { documentService } from '../services/documents.js';
|
|
15
|
+
import { notifyMessageLiked } from '../services/slack/notifications.js';
|
|
16
|
+
export const chatRouter = Router();
|
|
17
|
+
// Send message with streaming response
|
|
18
|
+
chatRouter.post('/', optionalAuth, optionalVerifiedEmail, async (req, res, next) => {
|
|
19
|
+
try {
|
|
20
|
+
const config = getConfig();
|
|
21
|
+
const parsed = sendMessageSchema.parse(req.body);
|
|
22
|
+
const { threadId, content, files, agentId: requestAgentId } = parsed;
|
|
23
|
+
// Extract teamId and projectId from request body (not in schema, optional)
|
|
24
|
+
// Only use teamId if teams feature is enabled
|
|
25
|
+
const teamsEnabled = config.teams?.enabled ?? false;
|
|
26
|
+
const projectsEnabled = config.projects?.enabled ?? false;
|
|
27
|
+
const requestTeamId = teamsEnabled ? req.body.teamId : undefined;
|
|
28
|
+
const requestProjectId = projectsEnabled ? req.body.projectId : undefined;
|
|
29
|
+
console.log(`[Chat] Received message: threadId=${threadId || 'new'}, agentId=${requestAgentId || 'none'}, content="${content.slice(0, 50)}${content.length > 50 ? '...' : ''}", files=${files?.length || 0}`);
|
|
30
|
+
// Check usage limits (will be done again after we know the threadId/teamId)
|
|
31
|
+
// Initial check uses personal limits - team limits checked after thread is resolved
|
|
32
|
+
// Get or create thread
|
|
33
|
+
let thread;
|
|
34
|
+
let threadAgentId = null;
|
|
35
|
+
if (threadId) {
|
|
36
|
+
thread = await db.thread.findUnique({
|
|
37
|
+
where: { id: threadId },
|
|
38
|
+
include: {
|
|
39
|
+
messages: {
|
|
40
|
+
orderBy: { createdAt: 'asc' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (!thread) {
|
|
45
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Thread not found');
|
|
46
|
+
}
|
|
47
|
+
// Check access - team threads require membership with write permission
|
|
48
|
+
if (thread.teamId) {
|
|
49
|
+
if (!req.user) {
|
|
50
|
+
throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required');
|
|
51
|
+
}
|
|
52
|
+
const membership = await db.teamMember.findUnique({
|
|
53
|
+
where: {
|
|
54
|
+
teamId_userId: {
|
|
55
|
+
teamId: thread.teamId,
|
|
56
|
+
userId: req.user.id,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!membership) {
|
|
61
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
62
|
+
}
|
|
63
|
+
// Viewers can read but not send messages
|
|
64
|
+
if (membership.role === 'viewer') {
|
|
65
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot send messages');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (thread.userId && thread.userId !== req.user?.id) {
|
|
69
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
70
|
+
}
|
|
71
|
+
// Use thread's agent (locked per thread)
|
|
72
|
+
threadAgentId = thread.agentId;
|
|
73
|
+
console.log(`[Chat] Existing thread ${threadId}, using locked agentId=${threadAgentId}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// New thread - use requested agent or default
|
|
77
|
+
if (requestAgentId) {
|
|
78
|
+
// Validate agent access
|
|
79
|
+
const agent = getAgentById(requestAgentId);
|
|
80
|
+
if (!agent) {
|
|
81
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Invalid agent');
|
|
82
|
+
}
|
|
83
|
+
threadAgentId = requestAgentId;
|
|
84
|
+
console.log(`[Chat] New thread with requested agentId=${requestAgentId}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const defaultAgent = getDefaultAgent();
|
|
88
|
+
threadAgentId = defaultAgent.id;
|
|
89
|
+
console.log(`[Chat] New thread with no agentId requested, using default: ${defaultAgent.id}`);
|
|
90
|
+
}
|
|
91
|
+
// Validate team membership if creating a team thread
|
|
92
|
+
if (requestTeamId) {
|
|
93
|
+
if (!req.user) {
|
|
94
|
+
throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required for team threads');
|
|
95
|
+
}
|
|
96
|
+
const membership = await db.teamMember.findUnique({
|
|
97
|
+
where: {
|
|
98
|
+
teamId_userId: {
|
|
99
|
+
teamId: requestTeamId,
|
|
100
|
+
userId: req.user.id,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
if (!membership) {
|
|
105
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'You are not a member of this team');
|
|
106
|
+
}
|
|
107
|
+
if (membership.role === 'viewer') {
|
|
108
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot create team threads');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Validate project access if creating a project thread
|
|
112
|
+
let effectiveTeamId = requestTeamId || null;
|
|
113
|
+
if (requestProjectId) {
|
|
114
|
+
if (!req.user) {
|
|
115
|
+
throw new AppError(HTTP_STATUS.UNAUTHORIZED, 'Authentication required for project threads');
|
|
116
|
+
}
|
|
117
|
+
const project = await db.project.findUnique({
|
|
118
|
+
where: { id: requestProjectId },
|
|
119
|
+
});
|
|
120
|
+
if (!project || project.archivedAt) {
|
|
121
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Project not found');
|
|
122
|
+
}
|
|
123
|
+
// Check project access
|
|
124
|
+
if (project.userId !== req.user.id) {
|
|
125
|
+
if (!project.teamId || project.sharing === 'private') {
|
|
126
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
127
|
+
}
|
|
128
|
+
// For team-shared projects, verify team membership and write permission
|
|
129
|
+
const membership = await db.teamMember.findUnique({
|
|
130
|
+
where: {
|
|
131
|
+
teamId_userId: {
|
|
132
|
+
teamId: project.teamId,
|
|
133
|
+
userId: req.user.id,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (!membership) {
|
|
138
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
139
|
+
}
|
|
140
|
+
if (membership.role === 'viewer') {
|
|
141
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Viewers cannot create threads');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Thread inherits teamId from project
|
|
145
|
+
effectiveTeamId = project.teamId;
|
|
146
|
+
}
|
|
147
|
+
thread = await db.thread.create({
|
|
148
|
+
data: {
|
|
149
|
+
title: content.slice(0, 50) + (content.length > 50 ? '...' : ''),
|
|
150
|
+
userId: req.user?.id,
|
|
151
|
+
teamId: effectiveTeamId,
|
|
152
|
+
projectId: requestProjectId || null,
|
|
153
|
+
agentId: threadAgentId,
|
|
154
|
+
},
|
|
155
|
+
include: { messages: true },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Get agent definition for this thread
|
|
159
|
+
const agentDef = getAgentById(threadAgentId);
|
|
160
|
+
console.log(`[Chat] Using agent: id=${agentDef?.id}, name=${agentDef?.name}, allowedTools=${agentDef?.allowedTools?.join(', ') || 'none'}`);
|
|
161
|
+
// Check usage limits with team context now that we have the thread
|
|
162
|
+
if (req.user) {
|
|
163
|
+
const canSend = await checkUsageLimits(req.user.id, thread.teamId || undefined);
|
|
164
|
+
if (!canSend) {
|
|
165
|
+
const context = thread.teamId ? 'team' : 'personal';
|
|
166
|
+
console.log(`[Chat] Usage limit exceeded for user ${req.user.id} (${context} billing)`);
|
|
167
|
+
throw new AppError(HTTP_STATUS.TOO_MANY_REQUESTS, `Usage limit exceeded for ${context} plan`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Create user message
|
|
171
|
+
await db.message.create({
|
|
172
|
+
data: {
|
|
173
|
+
threadId: thread.id,
|
|
174
|
+
role: 'user',
|
|
175
|
+
content,
|
|
176
|
+
files: files || undefined,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
// Update thread title if it's the first message and title is default
|
|
180
|
+
const isFirstMessage = thread.messages.length === 0;
|
|
181
|
+
if (isFirstMessage && thread.title === 'New Chat') {
|
|
182
|
+
const newTitle = content.slice(0, 50) + (content.length > 50 ? '...' : '');
|
|
183
|
+
await db.thread.update({
|
|
184
|
+
where: { id: thread.id },
|
|
185
|
+
data: { title: newTitle },
|
|
186
|
+
});
|
|
187
|
+
thread.title = newTitle;
|
|
188
|
+
console.log(`[Chat] Updated thread title to: ${newTitle}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(`[Chat] User message saved to thread ${thread.id}`);
|
|
191
|
+
// Set up SSE
|
|
192
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
193
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
194
|
+
res.setHeader('Connection', 'keep-alive');
|
|
195
|
+
res.flushHeaders();
|
|
196
|
+
// Send thread info (including title for sidebar update)
|
|
197
|
+
res.write(`data: ${JSON.stringify({ type: 'thread', threadId: thread.id, title: thread.title })}\n\n`);
|
|
198
|
+
// Create agent service from thread's agent definition
|
|
199
|
+
const agentConfig = agentDef ? toAgentConfig(agentDef) : config.agent;
|
|
200
|
+
console.log(`[Chat] Creating agent service for agent: ${agentDef?.name || 'default'} (${agentDef?.id || 'legacy'})`);
|
|
201
|
+
const agentService = createAgentService(agentConfig);
|
|
202
|
+
// Build conversation history
|
|
203
|
+
const history = thread.messages.map((msg) => ({
|
|
204
|
+
role: msg.role,
|
|
205
|
+
content: msg.content,
|
|
206
|
+
}));
|
|
207
|
+
history.push({ role: 'user', content });
|
|
208
|
+
// Get user context from settings
|
|
209
|
+
const userSettings = req.user
|
|
210
|
+
? await db.user.findUnique({
|
|
211
|
+
where: { id: req.user.id },
|
|
212
|
+
select: { settings: true },
|
|
213
|
+
})
|
|
214
|
+
: null;
|
|
215
|
+
const userContext = userSettings?.settings;
|
|
216
|
+
// Get team context if this is a team thread and teams are enabled
|
|
217
|
+
let teamContext = null;
|
|
218
|
+
if (teamsEnabled && thread.teamId) {
|
|
219
|
+
const team = await db.team.findUnique({
|
|
220
|
+
where: { id: thread.teamId },
|
|
221
|
+
select: { context: true },
|
|
222
|
+
});
|
|
223
|
+
teamContext = team?.context || null;
|
|
224
|
+
}
|
|
225
|
+
// Get project context if this thread belongs to a project and projects are enabled
|
|
226
|
+
let projectContext = null;
|
|
227
|
+
if (projectsEnabled && thread.projectId) {
|
|
228
|
+
const project = await db.project.findUnique({
|
|
229
|
+
where: { id: thread.projectId },
|
|
230
|
+
select: { context: true },
|
|
231
|
+
});
|
|
232
|
+
projectContext = project?.context || null;
|
|
233
|
+
}
|
|
234
|
+
// Parse mentions from message content and build mention context
|
|
235
|
+
let mentionContext = null;
|
|
236
|
+
const documentsEnabled = config.documents?.enabled ?? false;
|
|
237
|
+
const hybridThreshold = config.documents?.hybridThreshold ?? 1000;
|
|
238
|
+
if (documentsEnabled && req.user) {
|
|
239
|
+
const mentions = documentService.parseMentions(content);
|
|
240
|
+
if (mentions.length > 0) {
|
|
241
|
+
console.log(`[Chat] Found ${mentions.length} document mention(s) in message`);
|
|
242
|
+
const paths = mentions.map((m) => m.path);
|
|
243
|
+
const resolvedDocs = await documentService.resolveForContext(paths, req.user.id);
|
|
244
|
+
// Separate small docs (inject) and large docs (tools)
|
|
245
|
+
const smallDocs = resolvedDocs.filter((d) => d.charCount <= hybridThreshold);
|
|
246
|
+
const largeDocs = resolvedDocs.filter((d) => d.charCount > hybridThreshold);
|
|
247
|
+
// Build mention context for small docs
|
|
248
|
+
if (smallDocs.length > 0) {
|
|
249
|
+
mentionContext = 'Referenced documents:\n' +
|
|
250
|
+
smallDocs.map((d) => `--- ${d.path} ---\n${d.content}\n--- End ${d.path} ---`).join('\n\n');
|
|
251
|
+
console.log(`[Chat] Injected ${smallDocs.length} small document(s) into context`);
|
|
252
|
+
}
|
|
253
|
+
// Add hint about large docs that need tool access
|
|
254
|
+
if (largeDocs.length > 0) {
|
|
255
|
+
const largeDocHint = `\n\nLarge documents referenced (use read_document, search_in_document tools to access):\n` +
|
|
256
|
+
largeDocs.map((d) => `- ${d.path} (${d.charCount} chars)`).join('\n');
|
|
257
|
+
mentionContext = mentionContext
|
|
258
|
+
? mentionContext + largeDocHint
|
|
259
|
+
: largeDocHint;
|
|
260
|
+
console.log(`[Chat] ${largeDocs.length} large document(s) available via tools`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Track tools allowed for this thread (via "allow for this thread" scope)
|
|
265
|
+
const threadAllowedTools = [];
|
|
266
|
+
// Generate a visitor ID for this request (used for SSE connection lookup)
|
|
267
|
+
const visitorId = `visitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
268
|
+
// Get available MCP tools (including user-credential servers)
|
|
269
|
+
const mcpServers = config.mcp?.servers || [];
|
|
270
|
+
console.log(`[Chat] Fetching MCP tools for user ${req.user?.id || 'anonymous'}...`);
|
|
271
|
+
const allMcpTools = await mcpManager.listAllToolsForUser(req.user?.id, mcpServers);
|
|
272
|
+
// Get native tools available to this agent
|
|
273
|
+
const nativeTools = getNativeToolsForAgentFiltered(threadAgentId);
|
|
274
|
+
console.log(`[Chat] ${nativeTools.length} native tools available to agent`);
|
|
275
|
+
// Combine MCP and native tools, then filter based on agent's allowedTools
|
|
276
|
+
const allTools = [...allMcpTools, ...nativeTools];
|
|
277
|
+
const filteredTools = filterToolsForAgent(threadAgentId, allTools);
|
|
278
|
+
console.log(`[Chat] ${filteredTools.length} total tools available to agent (${allMcpTools.length} MCP + ${nativeTools.length} native, filtered by agent config)`);
|
|
279
|
+
if (filteredTools.length > 0) {
|
|
280
|
+
console.log(`[Chat] Tools: ${JSON.stringify(filteredTools.map(t => ({ name: t.name, server: t.serverId, desc: t.description?.slice(0, 50) })), null, 2)}`);
|
|
281
|
+
}
|
|
282
|
+
// Build a lookup for tool metadata (to find UI resources and identify native tools)
|
|
283
|
+
const toolMetaLookup = new Map();
|
|
284
|
+
for (const tool of filteredTools) {
|
|
285
|
+
toolMetaLookup.set(tool.name, { serverId: tool.serverId, _meta: '_meta' in tool ? tool._meta : undefined });
|
|
286
|
+
}
|
|
287
|
+
// Stream response with tool loop
|
|
288
|
+
let fullContent = '';
|
|
289
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
290
|
+
const toolCalls = [];
|
|
291
|
+
try {
|
|
292
|
+
console.log(`[Chat] Starting stream with ${history.length} messages in history`);
|
|
293
|
+
// Send start event
|
|
294
|
+
res.write(`data: ${JSON.stringify({ type: 'start' })}\n\n`);
|
|
295
|
+
console.log(`[Chat] Stream started, receiving chunks...`);
|
|
296
|
+
// Agentic loop - continues until no more tool calls
|
|
297
|
+
let conversationMessages = [...history];
|
|
298
|
+
let loopCount = 0;
|
|
299
|
+
const maxLoops = 10; // Prevent infinite loops
|
|
300
|
+
while (loopCount < maxLoops) {
|
|
301
|
+
loopCount++;
|
|
302
|
+
let currentLoopText = '';
|
|
303
|
+
const currentLoopToolCalls = [];
|
|
304
|
+
let stopReason;
|
|
305
|
+
// Get system prompt from agent definition
|
|
306
|
+
const systemPrompt = agentDef && isBuiltInAgent(agentDef) ? agentDef.systemPrompt : undefined;
|
|
307
|
+
const stream = agentService.chat(conversationMessages, {
|
|
308
|
+
systemPrompt,
|
|
309
|
+
teamContext,
|
|
310
|
+
projectContext,
|
|
311
|
+
mentionContext,
|
|
312
|
+
userContext,
|
|
313
|
+
files: loopCount === 1 ? files : undefined, // Only include files on first iteration
|
|
314
|
+
tools: filteredTools.length > 0 ? filteredTools : undefined,
|
|
315
|
+
});
|
|
316
|
+
let chunkCount = 0;
|
|
317
|
+
for await (const chunk of stream) {
|
|
318
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
319
|
+
currentLoopText += chunk.content;
|
|
320
|
+
fullContent += chunk.content;
|
|
321
|
+
res.write(`data: ${JSON.stringify({ type: 'delta', content: chunk.content })}\n\n`);
|
|
322
|
+
chunkCount++;
|
|
323
|
+
}
|
|
324
|
+
else if (chunk.type === 'tool_use' && chunk.toolUse) {
|
|
325
|
+
// Send tool_use event to frontend
|
|
326
|
+
console.log(`[Chat] Sending tool_use event: ${chunk.toolUse.name} (id: ${chunk.toolUse.id})`);
|
|
327
|
+
res.write(`data: ${JSON.stringify({
|
|
328
|
+
type: 'tool_use',
|
|
329
|
+
id: chunk.toolUse.id,
|
|
330
|
+
name: chunk.toolUse.name,
|
|
331
|
+
serverId: chunk.toolUse.serverId,
|
|
332
|
+
input: chunk.toolUse.input,
|
|
333
|
+
})}\n\n`);
|
|
334
|
+
currentLoopToolCalls.push({
|
|
335
|
+
id: chunk.toolUse.id,
|
|
336
|
+
name: chunk.toolUse.name,
|
|
337
|
+
serverId: chunk.toolUse.serverId,
|
|
338
|
+
input: chunk.toolUse.input,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
else if (chunk.type === 'stop' && chunk.stopReason) {
|
|
342
|
+
stopReason = chunk.stopReason;
|
|
343
|
+
}
|
|
344
|
+
else if (chunk.type === 'usage' && chunk.usage) {
|
|
345
|
+
totalUsage.inputTokens += chunk.usage.inputTokens;
|
|
346
|
+
totalUsage.outputTokens += chunk.usage.outputTokens;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log(`[Chat] Loop ${loopCount}: ${chunkCount} text chunks, ${currentLoopToolCalls.length} tool calls, stop=${stopReason}`);
|
|
350
|
+
// If no tool calls or stop reason is not tool_use, we're done
|
|
351
|
+
if (currentLoopToolCalls.length === 0 || stopReason !== 'tool_use') {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
// Execute tool calls and collect results
|
|
355
|
+
const toolResultBlocks = [];
|
|
356
|
+
for (const toolCall of currentLoopToolCalls) {
|
|
357
|
+
console.log(`[Chat] Executing tool: ${toolCall.name} on server ${toolCall.serverId}`);
|
|
358
|
+
const isNativeToolCall = isNativeTool(toolCall.serverId);
|
|
359
|
+
// Find server config for user-credential support (not needed for native tools)
|
|
360
|
+
const serverConfig = isNativeToolCall ? undefined : mcpServers.find((s) => s.id === toolCall.serverId);
|
|
361
|
+
if (!isNativeToolCall && !serverConfig) {
|
|
362
|
+
console.error(`[Chat] Server config not found for ${toolCall.serverId}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const toolId = `${toolCall.serverId}:${toolCall.name}`;
|
|
366
|
+
// Check if tool confirmation is required
|
|
367
|
+
const confirmCheck = checkToolConfirmationRequired({
|
|
368
|
+
serverId: toolCall.serverId,
|
|
369
|
+
toolName: toolCall.name,
|
|
370
|
+
userId: req.user?.id || '',
|
|
371
|
+
userSettings: userContext || undefined,
|
|
372
|
+
threadAllowedTools,
|
|
373
|
+
});
|
|
374
|
+
let toolResult;
|
|
375
|
+
if (confirmCheck.required && req.user) {
|
|
376
|
+
// Tool requires confirmation - create pending and wait for user response
|
|
377
|
+
console.log(`[Chat] Tool ${toolId} requires confirmation`);
|
|
378
|
+
// Send pending confirmation event to frontend
|
|
379
|
+
const { id: confirmationId, promise: confirmationPromise } = pendingConfirmations.create({
|
|
380
|
+
visitorId,
|
|
381
|
+
threadId: thread.id,
|
|
382
|
+
userId: req.user.id,
|
|
383
|
+
serverId: toolCall.serverId,
|
|
384
|
+
toolName: toolCall.name,
|
|
385
|
+
toolArgs: toolCall.input,
|
|
386
|
+
});
|
|
387
|
+
res.write(`data: ${JSON.stringify({
|
|
388
|
+
type: 'tool_pending_confirmation',
|
|
389
|
+
confirmationId,
|
|
390
|
+
serverId: toolCall.serverId,
|
|
391
|
+
toolName: toolCall.name,
|
|
392
|
+
toolArgs: toolCall.input,
|
|
393
|
+
})}\n\n`);
|
|
394
|
+
// Wait for user response
|
|
395
|
+
const confirmationResult = await confirmationPromise;
|
|
396
|
+
// Send confirmation result event
|
|
397
|
+
res.write(`data: ${JSON.stringify({
|
|
398
|
+
type: 'tool_confirmed',
|
|
399
|
+
confirmationId,
|
|
400
|
+
approved: confirmationResult.approved,
|
|
401
|
+
})}\n\n`);
|
|
402
|
+
if (!confirmationResult.approved) {
|
|
403
|
+
// User denied - create denial result
|
|
404
|
+
console.log(`[Chat] User denied tool ${toolId}`);
|
|
405
|
+
toolResult = {
|
|
406
|
+
content: [{ type: 'text', text: createDenialToolResult(toolCall.name) }],
|
|
407
|
+
isError: false, // Not an error, just a denial
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// User approved - check scope and execute
|
|
412
|
+
if (confirmationResult.scope === 'thread') {
|
|
413
|
+
// Add to thread-level allowlist
|
|
414
|
+
threadAllowedTools.push(toolId);
|
|
415
|
+
console.log(`[Chat] Added ${toolId} to thread allowlist`);
|
|
416
|
+
}
|
|
417
|
+
// Note: 'always' scope is handled in the /confirm-tool endpoint
|
|
418
|
+
// Execute the tool
|
|
419
|
+
if (isNativeToolCall) {
|
|
420
|
+
toolResult = await executeNativeTool(toolCall.name, toolCall.input, {
|
|
421
|
+
userId: req.user?.id,
|
|
422
|
+
threadId: thread.id,
|
|
423
|
+
agentId: threadAgentId || undefined,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
toolResult = await mcpManager.callToolForUser(req.user?.id, toolCall.serverId, toolCall.name, toolCall.input, serverConfig);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// Tool is auto-approved or no user context
|
|
433
|
+
if (confirmCheck.autoApproveReason && req.user) {
|
|
434
|
+
console.log(`[Chat] Tool ${toolId} auto-approved: ${confirmCheck.autoApproveReason}`);
|
|
435
|
+
res.write(`data: ${JSON.stringify({
|
|
436
|
+
type: 'tool_auto_approved',
|
|
437
|
+
toolCallId: toolCall.id,
|
|
438
|
+
serverId: toolCall.serverId,
|
|
439
|
+
toolName: toolCall.name,
|
|
440
|
+
reason: confirmCheck.autoApproveReason,
|
|
441
|
+
})}\n\n`);
|
|
442
|
+
}
|
|
443
|
+
// Execute the tool
|
|
444
|
+
if (isNativeToolCall) {
|
|
445
|
+
toolResult = await executeNativeTool(toolCall.name, toolCall.input, {
|
|
446
|
+
userId: req.user?.id,
|
|
447
|
+
threadId: thread.id,
|
|
448
|
+
agentId: threadAgentId || undefined,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
toolResult = await mcpManager.callToolForUser(req.user?.id, toolCall.serverId, toolCall.name, toolCall.input, serverConfig);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const result = toolResult;
|
|
456
|
+
// Log the full result from callToolForUser
|
|
457
|
+
console.log(`[Chat] Tool ${toolCall.name} result:`, {
|
|
458
|
+
contentLength: result.content?.length,
|
|
459
|
+
contentPreview: result.content?.map(c => c.text?.slice(0, 100) || c.type),
|
|
460
|
+
hasStructuredContent: !!result.structuredContent,
|
|
461
|
+
structuredContentKeys: result.structuredContent ? Object.keys(result.structuredContent) : null,
|
|
462
|
+
isError: result.isError,
|
|
463
|
+
});
|
|
464
|
+
// Check if tool has a UI resource template to fetch
|
|
465
|
+
const toolMeta = toolMetaLookup.get(toolCall.name);
|
|
466
|
+
console.log(`[Chat] Tool meta lookup for ${toolCall.name}:`, toolMeta ? { hasMetadata: !!toolMeta._meta, keys: toolMeta._meta ? Object.keys(toolMeta._meta) : [] } : 'NOT FOUND');
|
|
467
|
+
let uiResource = null;
|
|
468
|
+
let resourceUri;
|
|
469
|
+
let isOpenAiFormat = false;
|
|
470
|
+
// Check for native tool templates first
|
|
471
|
+
if (isNativeToolCall) {
|
|
472
|
+
const nativeTemplate = await resolveNativeToolTemplate(toolCall.name);
|
|
473
|
+
if (nativeTemplate) {
|
|
474
|
+
uiResource = nativeTemplate;
|
|
475
|
+
resourceUri = `native://${toolCall.name}/template`;
|
|
476
|
+
isOpenAiFormat = true; // Use OpenAI format for consistent rendering
|
|
477
|
+
console.log(`[Chat] Resolved native tool template for ${toolCall.name}, length=${nativeTemplate.text.length}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// MCP tool - check for resource URI
|
|
482
|
+
const openaiOutputTemplate = toolMeta?._meta?.['openai/outputTemplate'];
|
|
483
|
+
const uiResourceUri = toolMeta?._meta?.['ui/resourceUri'];
|
|
484
|
+
resourceUri = (openaiOutputTemplate || uiResourceUri);
|
|
485
|
+
console.log(`[Chat] UI resource URI for ${toolCall.name}: ${resourceUri || 'none'} (openai: ${openaiOutputTemplate}, ui: ${uiResourceUri})`);
|
|
486
|
+
isOpenAiFormat = !!openaiOutputTemplate;
|
|
487
|
+
if (resourceUri && typeof resourceUri === 'string') {
|
|
488
|
+
console.log(`[Chat] Tool ${toolCall.name} has UI resource: ${resourceUri} (openai format: ${isOpenAiFormat})`);
|
|
489
|
+
uiResource = await mcpManager.readResourceForUser(req.user?.id || '', toolCall.serverId, resourceUri, serverConfig);
|
|
490
|
+
if (uiResource) {
|
|
491
|
+
console.log(`[Chat] Fetched UI resource: ${resourceUri}, mimeType=${uiResource.mimeType}, length=${uiResource.text?.length || uiResource.blob?.length || 0}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Send tool_result event to frontend (include UI resource if available)
|
|
496
|
+
// Use structuredContent as toolOutput if available, otherwise fall back to content
|
|
497
|
+
const toolOutput = result.structuredContent || result.content;
|
|
498
|
+
const toolResultEvent = {
|
|
499
|
+
type: 'tool_result',
|
|
500
|
+
id: toolCall.id,
|
|
501
|
+
name: toolCall.name,
|
|
502
|
+
serverId: toolCall.serverId,
|
|
503
|
+
input: toolCall.input,
|
|
504
|
+
content: result.content,
|
|
505
|
+
isError: result.isError,
|
|
506
|
+
uiResource: uiResource ? {
|
|
507
|
+
uri: resourceUri,
|
|
508
|
+
text: uiResource.text,
|
|
509
|
+
blob: uiResource.blob,
|
|
510
|
+
mimeType: uiResource.mimeType,
|
|
511
|
+
isOpenAiFormat,
|
|
512
|
+
toolInput: toolCall.input,
|
|
513
|
+
toolOutput,
|
|
514
|
+
} : undefined,
|
|
515
|
+
};
|
|
516
|
+
console.log(`[Chat] Sending tool_result event:`, {
|
|
517
|
+
id: toolResultEvent.id,
|
|
518
|
+
hasContent: !!toolResultEvent.content?.length,
|
|
519
|
+
hasUiResource: !!toolResultEvent.uiResource,
|
|
520
|
+
uiResourceMimeType: toolResultEvent.uiResource?.mimeType,
|
|
521
|
+
uiResourceTextLength: toolResultEvent.uiResource?.text?.length,
|
|
522
|
+
});
|
|
523
|
+
res.write(`data: ${JSON.stringify(toolResultEvent)}\n\n`);
|
|
524
|
+
// Store for message history (including UI resource for persistence)
|
|
525
|
+
toolCalls.push({
|
|
526
|
+
...toolCall,
|
|
527
|
+
result: result.content,
|
|
528
|
+
isError: result.isError,
|
|
529
|
+
uiResource: uiResource ? {
|
|
530
|
+
uri: resourceUri,
|
|
531
|
+
text: uiResource.text,
|
|
532
|
+
blob: uiResource.blob,
|
|
533
|
+
mimeType: uiResource.mimeType,
|
|
534
|
+
isOpenAiFormat,
|
|
535
|
+
toolInput: toolCall.input,
|
|
536
|
+
toolOutput,
|
|
537
|
+
} : undefined,
|
|
538
|
+
});
|
|
539
|
+
// Build tool result block for next iteration
|
|
540
|
+
// Include both content text and structuredContent for full context
|
|
541
|
+
let resultText = result.content
|
|
542
|
+
.map((c) => c.text || JSON.stringify(c))
|
|
543
|
+
.join('\n');
|
|
544
|
+
// Append structuredContent if available (this is the rich data the widget renders)
|
|
545
|
+
if (result.structuredContent) {
|
|
546
|
+
resultText += '\n\nThe user has been shown this structured data:\n' + JSON.stringify(result.structuredContent, null, 2);
|
|
547
|
+
}
|
|
548
|
+
console.log(`[Chat] Tool result text for agent (length=${resultText.length}):`, resultText.slice(0, 500) + (resultText.length > 500 ? '...' : ''));
|
|
549
|
+
toolResultBlocks.push({
|
|
550
|
+
type: 'tool_result',
|
|
551
|
+
tool_use_id: toolCall.id,
|
|
552
|
+
content: resultText,
|
|
553
|
+
is_error: result.isError,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
// Build assistant message with tool uses for context
|
|
557
|
+
const assistantContent = [];
|
|
558
|
+
if (currentLoopText) {
|
|
559
|
+
assistantContent.push({ type: 'text', text: currentLoopText });
|
|
560
|
+
}
|
|
561
|
+
for (const tc of currentLoopToolCalls) {
|
|
562
|
+
assistantContent.push({
|
|
563
|
+
type: 'tool_use',
|
|
564
|
+
id: tc.id,
|
|
565
|
+
name: tc.name,
|
|
566
|
+
input: tc.input,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// Add assistant message with tool uses
|
|
570
|
+
conversationMessages.push({
|
|
571
|
+
role: 'assistant',
|
|
572
|
+
content: assistantContent,
|
|
573
|
+
});
|
|
574
|
+
// Add tool results as user message
|
|
575
|
+
conversationMessages.push({
|
|
576
|
+
role: 'user',
|
|
577
|
+
content: toolResultBlocks,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
console.log(`[Chat] Stream complete after ${loopCount} loops: ${fullContent.length} chars, ${toolCalls.length} tool calls`);
|
|
581
|
+
// Create assistant message with tool calls
|
|
582
|
+
const assistantMessage = await db.message.create({
|
|
583
|
+
data: {
|
|
584
|
+
threadId: thread.id,
|
|
585
|
+
role: 'assistant',
|
|
586
|
+
content: fullContent,
|
|
587
|
+
inputTokens: totalUsage.inputTokens,
|
|
588
|
+
outputTokens: totalUsage.outputTokens,
|
|
589
|
+
toolCalls: toolCalls.length > 0 ? JSON.parse(JSON.stringify(toolCalls.map((tc) => ({
|
|
590
|
+
id: tc.id,
|
|
591
|
+
serverId: tc.serverId,
|
|
592
|
+
toolName: tc.name,
|
|
593
|
+
arguments: tc.input,
|
|
594
|
+
status: tc.isError ? 'error' : 'completed',
|
|
595
|
+
})))) : undefined,
|
|
596
|
+
toolResults: toolCalls.length > 0
|
|
597
|
+
? JSON.parse(JSON.stringify(toolCalls.map((tc) => ({
|
|
598
|
+
toolCallId: tc.id,
|
|
599
|
+
content: tc.result,
|
|
600
|
+
isError: tc.isError,
|
|
601
|
+
uiResource: tc.uiResource,
|
|
602
|
+
}))))
|
|
603
|
+
: undefined,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
// Update thread timestamp
|
|
607
|
+
await db.thread.update({
|
|
608
|
+
where: { id: thread.id },
|
|
609
|
+
data: { updatedAt: new Date() },
|
|
610
|
+
});
|
|
611
|
+
// Increment usage on the correct entity (user or team)
|
|
612
|
+
if (req.user) {
|
|
613
|
+
await incrementUsage(req.user.id, thread.teamId || undefined);
|
|
614
|
+
}
|
|
615
|
+
// Send done event
|
|
616
|
+
res.write(`data: ${JSON.stringify({
|
|
617
|
+
type: 'done',
|
|
618
|
+
messageId: assistantMessage.id,
|
|
619
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
620
|
+
usage: {
|
|
621
|
+
inputTokens: totalUsage.inputTokens,
|
|
622
|
+
outputTokens: totalUsage.outputTokens,
|
|
623
|
+
totalTokens: totalUsage.inputTokens + totalUsage.outputTokens,
|
|
624
|
+
},
|
|
625
|
+
})}\n\n`);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
console.error('Chat error:', error);
|
|
629
|
+
res.write(`data: ${JSON.stringify({
|
|
630
|
+
type: 'error',
|
|
631
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
632
|
+
})}\n\n`);
|
|
633
|
+
}
|
|
634
|
+
res.end();
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
next(error);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
// Regenerate assistant message
|
|
641
|
+
chatRouter.post('/regenerate/:messageId', requireAuth, async (req, res, next) => {
|
|
642
|
+
try {
|
|
643
|
+
const { messageId } = req.params;
|
|
644
|
+
const config = getConfig();
|
|
645
|
+
const message = await db.message.findUnique({
|
|
646
|
+
where: { id: messageId },
|
|
647
|
+
include: {
|
|
648
|
+
thread: {
|
|
649
|
+
include: {
|
|
650
|
+
messages: {
|
|
651
|
+
orderBy: { createdAt: 'asc' },
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
if (!message) {
|
|
658
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
|
|
659
|
+
}
|
|
660
|
+
if (message.role !== 'assistant') {
|
|
661
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Can only regenerate assistant messages');
|
|
662
|
+
}
|
|
663
|
+
if (message.thread.userId !== req.user.id) {
|
|
664
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
665
|
+
}
|
|
666
|
+
// Check usage limits
|
|
667
|
+
const canSend = await checkUsageLimits(req.user.id);
|
|
668
|
+
if (!canSend) {
|
|
669
|
+
throw new AppError(HTTP_STATUS.TOO_MANY_REQUESTS, 'Usage limit exceeded');
|
|
670
|
+
}
|
|
671
|
+
// Set up SSE
|
|
672
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
673
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
674
|
+
res.setHeader('Connection', 'keep-alive');
|
|
675
|
+
res.flushHeaders();
|
|
676
|
+
// Build history up to this message (excluding the message to regenerate)
|
|
677
|
+
const messageIndex = message.thread.messages.findIndex((m) => m.id === messageId);
|
|
678
|
+
const history = message.thread.messages.slice(0, messageIndex).map((msg) => ({
|
|
679
|
+
role: msg.role,
|
|
680
|
+
content: msg.content,
|
|
681
|
+
}));
|
|
682
|
+
// Get agent for this thread
|
|
683
|
+
const agentDef = getAgentById(message.thread.agentId);
|
|
684
|
+
const agentConfig = agentDef ? toAgentConfig(agentDef) : config.agent;
|
|
685
|
+
const agentService = createAgentService(agentConfig);
|
|
686
|
+
let fullContent = '';
|
|
687
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
688
|
+
try {
|
|
689
|
+
// Get system prompt from agent definition
|
|
690
|
+
const systemPrompt = agentDef && isBuiltInAgent(agentDef) ? agentDef.systemPrompt : undefined;
|
|
691
|
+
const stream = await agentService.chat(history, {
|
|
692
|
+
systemPrompt,
|
|
693
|
+
});
|
|
694
|
+
res.write(`data: ${JSON.stringify({ type: 'start' })}\n\n`);
|
|
695
|
+
for await (const chunk of stream) {
|
|
696
|
+
if (chunk.type === 'text') {
|
|
697
|
+
fullContent += chunk.content;
|
|
698
|
+
res.write(`data: ${JSON.stringify({ type: 'delta', content: chunk.content })}\n\n`);
|
|
699
|
+
}
|
|
700
|
+
else if (chunk.type === 'usage' && chunk.usage) {
|
|
701
|
+
usage = chunk.usage;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Update the message
|
|
705
|
+
await db.message.update({
|
|
706
|
+
where: { id: messageId },
|
|
707
|
+
data: {
|
|
708
|
+
content: fullContent,
|
|
709
|
+
inputTokens: usage.inputTokens,
|
|
710
|
+
outputTokens: usage.outputTokens,
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
// Increment usage on the correct entity (user or team)
|
|
714
|
+
await incrementUsage(req.user.id, message.thread.teamId || undefined);
|
|
715
|
+
res.write(`data: ${JSON.stringify({
|
|
716
|
+
type: 'done',
|
|
717
|
+
messageId,
|
|
718
|
+
usage: {
|
|
719
|
+
inputTokens: usage.inputTokens,
|
|
720
|
+
outputTokens: usage.outputTokens,
|
|
721
|
+
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
722
|
+
},
|
|
723
|
+
})}\n\n`);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
res.write(`data: ${JSON.stringify({
|
|
727
|
+
type: 'error',
|
|
728
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
729
|
+
})}\n\n`);
|
|
730
|
+
}
|
|
731
|
+
res.end();
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
next(error);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
// Branch from message
|
|
738
|
+
chatRouter.post('/branch/:messageId', requireAuth, async (req, res, next) => {
|
|
739
|
+
try {
|
|
740
|
+
const { messageId } = req.params;
|
|
741
|
+
const message = await db.message.findUnique({
|
|
742
|
+
where: { id: messageId },
|
|
743
|
+
include: {
|
|
744
|
+
thread: {
|
|
745
|
+
include: {
|
|
746
|
+
messages: {
|
|
747
|
+
orderBy: { createdAt: 'asc' },
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
if (!message) {
|
|
754
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
|
|
755
|
+
}
|
|
756
|
+
if (message.thread.userId !== req.user.id) {
|
|
757
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Access denied');
|
|
758
|
+
}
|
|
759
|
+
// Create new thread as branch, inheriting the agent from parent
|
|
760
|
+
const newThread = await db.thread.create({
|
|
761
|
+
data: {
|
|
762
|
+
title: `Branch from: ${message.thread.title}`,
|
|
763
|
+
userId: req.user.id,
|
|
764
|
+
agentId: message.thread.agentId,
|
|
765
|
+
parentThreadId: message.thread.id,
|
|
766
|
+
parentMessageId: messageId,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
// Copy messages up to and including the branch point
|
|
770
|
+
const messageIndex = message.thread.messages.findIndex((m) => m.id === messageId);
|
|
771
|
+
const messagesToCopy = message.thread.messages.slice(0, messageIndex + 1);
|
|
772
|
+
for (const msg of messagesToCopy) {
|
|
773
|
+
await db.message.create({
|
|
774
|
+
data: {
|
|
775
|
+
threadId: newThread.id,
|
|
776
|
+
role: msg.role,
|
|
777
|
+
content: msg.content,
|
|
778
|
+
files: msg.files || undefined,
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// Note: We don't add the new user message here - the frontend will send it
|
|
783
|
+
// via the normal chat endpoint, which triggers the AI response
|
|
784
|
+
// Fetch the thread with all messages for the response
|
|
785
|
+
const threadWithMessages = await db.thread.findUnique({
|
|
786
|
+
where: { id: newThread.id },
|
|
787
|
+
include: {
|
|
788
|
+
messages: {
|
|
789
|
+
orderBy: { createdAt: 'asc' },
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
// Get agent info
|
|
794
|
+
const agent = getAgentById(newThread.agentId);
|
|
795
|
+
res.status(HTTP_STATUS.CREATED).json({
|
|
796
|
+
thread: {
|
|
797
|
+
...threadWithMessages,
|
|
798
|
+
agentName: agent?.name,
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
next(error);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
// Submit feedback for a message
|
|
807
|
+
chatRouter.post('/feedback/:messageId', requireAuth, async (req, res, next) => {
|
|
808
|
+
try {
|
|
809
|
+
const { messageId } = req.params;
|
|
810
|
+
const { type, comment } = req.body;
|
|
811
|
+
if (!['up', 'down'].includes(type)) {
|
|
812
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Invalid feedback type');
|
|
813
|
+
}
|
|
814
|
+
const message = await db.message.findUnique({
|
|
815
|
+
where: { id: messageId },
|
|
816
|
+
include: { thread: true },
|
|
817
|
+
});
|
|
818
|
+
if (!message) {
|
|
819
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Message not found');
|
|
820
|
+
}
|
|
821
|
+
// Upsert feedback
|
|
822
|
+
const feedback = await db.messageFeedback.upsert({
|
|
823
|
+
where: {
|
|
824
|
+
messageId_userId: {
|
|
825
|
+
messageId,
|
|
826
|
+
userId: req.user.id,
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
update: { type, comment },
|
|
830
|
+
create: {
|
|
831
|
+
messageId,
|
|
832
|
+
userId: req.user.id,
|
|
833
|
+
type,
|
|
834
|
+
comment,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
// Send Slack notification for positive feedback on team threads
|
|
838
|
+
if (type === 'up' && message.thread.teamId) {
|
|
839
|
+
notifyMessageLiked(message.thread.teamId, req.user.name, req.user.email, message.thread.title).catch(err => console.error('[Feedback] Slack notification failed:', err));
|
|
840
|
+
}
|
|
841
|
+
res.json({ feedback });
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
next(error);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
// Confirm or deny a tool call
|
|
848
|
+
chatRouter.post('/confirm-tool', requireAuth, async (req, res, next) => {
|
|
849
|
+
try {
|
|
850
|
+
const { confirmationId, approved, scope } = req.body;
|
|
851
|
+
if (!confirmationId) {
|
|
852
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'confirmationId is required');
|
|
853
|
+
}
|
|
854
|
+
if (typeof approved !== 'boolean') {
|
|
855
|
+
throw new AppError(HTTP_STATUS.BAD_REQUEST, 'approved must be a boolean');
|
|
856
|
+
}
|
|
857
|
+
const pending = pendingConfirmations.get(confirmationId);
|
|
858
|
+
if (!pending) {
|
|
859
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Confirmation not found or expired');
|
|
860
|
+
}
|
|
861
|
+
// Verify the user owns this confirmation
|
|
862
|
+
if (pending.userId !== req.user.id) {
|
|
863
|
+
throw new AppError(HTTP_STATUS.FORBIDDEN, 'Not authorized to confirm this tool call');
|
|
864
|
+
}
|
|
865
|
+
// If "always allow", update user settings
|
|
866
|
+
if (approved && scope === 'always') {
|
|
867
|
+
const toolId = `${pending.serverId}:${pending.toolName}`;
|
|
868
|
+
const user = await db.user.findUnique({
|
|
869
|
+
where: { id: req.user.id },
|
|
870
|
+
select: { settings: true },
|
|
871
|
+
});
|
|
872
|
+
const currentSettings = user?.settings || {};
|
|
873
|
+
const allowedTools = currentSettings.allowedTools || [];
|
|
874
|
+
// Only add if not already present
|
|
875
|
+
if (!allowedTools.includes(toolId)) {
|
|
876
|
+
await db.user.update({
|
|
877
|
+
where: { id: req.user.id },
|
|
878
|
+
data: {
|
|
879
|
+
settings: {
|
|
880
|
+
...currentSettings,
|
|
881
|
+
allowedTools: [...allowedTools, toolId],
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
console.log(`[ConfirmTool] Added ${toolId} to user ${req.user.id}'s allowedTools`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Resolve the pending confirmation
|
|
889
|
+
const resolved = pendingConfirmations.resolve(confirmationId, approved, scope);
|
|
890
|
+
if (!resolved) {
|
|
891
|
+
throw new AppError(HTTP_STATUS.NOT_FOUND, 'Failed to resolve confirmation');
|
|
892
|
+
}
|
|
893
|
+
console.log(`[ConfirmTool] User ${req.user.id} ${approved ? 'approved' : 'denied'} tool ${pending.serverId}:${pending.toolName} (scope: ${scope || 'once'})`);
|
|
894
|
+
res.json({ success: true });
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
next(error);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
//# sourceMappingURL=chat.js.map
|