-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathassistant.ts
More file actions
112 lines (98 loc) · 3.44 KB
/
assistant.ts
File metadata and controls
112 lines (98 loc) · 3.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { buildSystemPrompt, createAssistantTools } from '@/modules/assistant';
import { EnvVariables } from '@/server/iface';
import { authMiddleware, getToken, getUserId } from '@/server/middleware';
import { env } from '@/utils/config/env.server';
import { logger } from '@/utils/logger';
import { createAnthropic } from '@ai-sdk/anthropic';
import { convertToModelMessages, smoothStream, stepCountIs, streamText, type UIMessage } from 'ai';
import { Hono } from 'hono';
export const assistantRoutes = new Hono<{ Variables: EnvVariables }>();
const MAX_MESSAGES = 50;
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
const RATE_LIMIT_WINDOW_MS = 60_000;
const RATE_LIMIT_MAX = 20;
function checkRateLimit(userId: string): boolean {
const now = Date.now();
const entry = rateLimitMap.get(userId);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(userId, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return true;
}
if (entry.count >= RATE_LIMIT_MAX) return false;
entry.count++;
return true;
}
assistantRoutes.post('/', authMiddleware(), async (c) => {
if (!env.chatbotEnabled || !env.anthropicApiKey) {
return c.json({ error: 'AI assistant is not configured' }, 503);
}
const token = getToken(c);
const userId = getUserId(c);
const body = await c.req.json();
const { messages, clientOs } = body as {
messages: UIMessage[];
clientOs?: string;
};
const lastUserMessage = messages.findLast((m) => m.role === 'user');
const userMessage = lastUserMessage?.parts
?.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
.map((p) => p.text)
.join(' ')
.slice(0, 500);
if (!checkRateLimit(userId)) {
return c.json({ error: 'Too Many Requests' }, 429);
}
try {
const anthropic = createAnthropic({ apiKey: env.anthropicApiKey });
const model = env.anthropicModel ?? 'claude-sonnet-4-6';
const result = streamText({
model: anthropic(model),
system: buildSystemPrompt(clientOs),
messages: await convertToModelMessages(messages.slice(-MAX_MESSAGES)),
maxOutputTokens: 4096,
experimental_transform: smoothStream({ chunking: 'word', delayInMs: 40 }),
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 10000,
},
metadata: { user_id: userId },
},
},
stopWhen: stepCountIs(10),
tools: createAssistantTools({ accessToken: token }),
});
result.response.then(undefined, (err: unknown) => {
logger.error('assistant stream failed', {
userId,
model,
userMessage,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
});
result.usage.then(
(usage) => {
logger.info('assistant request completed', {
userId,
model,
userMessage,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
});
},
() => {}
);
return result.toUIMessageStreamResponse({ sendReasoning: true });
} catch (err) {
logger.error('assistant request failed', {
userId,
userMessage,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
return c.json({ error: 'Failed to start assistant' }, 500);
}
});