Skip to content

Commit d4ef043

Browse files
committed
feat(chat): instrument LLM usage
1 parent f8f7049 commit d4ef043

File tree

3 files changed

+133
-107
lines changed

3 files changed

+133
-107
lines changed

app/api/chat/route.ts

Lines changed: 114 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { graphql } from "@/gql";
22
import { getClient } from "@/lib/apollo.rsc";
3-
import { checkAuthorizedStatus } from "@/lib/auth.rsc";
3+
import { getAuthorizedUserInfo } from "@/lib/auth.rsc";
44
import { anthropic, type AnthropicProviderOptions } from "@ai-sdk/anthropic";
55
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";
6+
import { withTracing } from "@posthog/ai"
67
import { NextResponse } from "next/server";
78
import { z } from "zod";
9+
import { createPostHogClient } from "@/lib/posthog.rsc";
810

911
export const maxDuration = 30;
1012

@@ -72,8 +74,8 @@ interface ChatRouteRequest {
7274
}
7375

7476
export async function POST(req: Request) {
75-
const authorized = await checkAuthorizedStatus(["*", "ai"]);
76-
if (!authorized) {
77+
const userInfo = await getAuthorizedUserInfo(["*", "ai"]);
78+
if (!userInfo) {
7779
return new NextResponse("Unauthorized", { status: 401 });
7880
}
7981

@@ -98,104 +100,115 @@ export async function POST(req: Request) {
98100
.replace("{{QUESTION_DIFFICULTY}}", data.question.difficulty)
99101
.replace("{{QUESTION_CATEGORY}}", data.question.category);
100102

101-
const result = streamText({
102-
model: anthropic("claude-sonnet-4-20250514"),
103-
providerOptions: {
104-
anthropic: {
105-
thinking: { type: "enabled", budgetTokens: 12000 },
106-
} satisfies AnthropicProviderOptions,
107-
},
108-
messages: convertToModelMessages(messages),
109-
system: preparedPrompt,
110-
stopWhen: stepCountIs(10),
111-
tools: {
112-
getMyAnswer: tool({
113-
description:
114-
"取得使用者最後提交的答案結果,包括查詢結果、錯誤訊息和狀態。如果使用者問關於他們的答案,使用這個工具。",
115-
inputSchema: z.object({}),
116-
execute: async () => {
117-
const { data, error } = await apollo.query({
118-
query: USER_ANSWER_RESULT,
119-
variables: { id: questionId },
120-
errorPolicy: "all",
121-
});
122-
if (!data?.question) {
123-
return { error: "無法取得題目資料", details: error?.message };
124-
}
125-
126-
const { lastSubmission } = data.question;
127-
if (!lastSubmission) {
128-
return { error: "使用者尚未提交答案" };
129-
}
130-
131-
return {
132-
status: lastSubmission.status,
133-
submittedCode: lastSubmission.submittedCode,
134-
queryResult: lastSubmission.queryResult
135-
? {
136-
columns: lastSubmission.queryResult.columns,
137-
rows: lastSubmission.queryResult.rows,
138-
}
139-
: null,
140-
error: lastSubmission.error,
141-
};
142-
},
143-
}),
144-
getCorrectAnswer: tool({
145-
description: "取得題目的正確答案結果,你可以對照和使用者的答案差異。",
146-
inputSchema: z.object({}),
147-
execute: async () => {
148-
const { data, error } = await apollo.query({
149-
query: CORRECT_ANSWER_RESULT,
150-
variables: { id: questionId },
151-
errorPolicy: "all",
152-
});
153-
if (!data?.question) {
154-
return { error: "無法取得題目資料", details: error?.message };
155-
}
156-
157-
return {
158-
queryResult: data.question.referenceAnswerResult
159-
? {
160-
columns: data.question.referenceAnswerResult.columns,
161-
rows: data.question.referenceAnswerResult.rows,
162-
}
163-
: null,
164-
};
165-
},
166-
}),
167-
getQuestionSchema: tool({
168-
description: "取得題目的資料庫結構,你可以用這個數據輔助了解 SQL 結構。",
169-
inputSchema: z.object({}),
170-
execute: async () => {
171-
const { data, error } = await apollo.query({
172-
query: QUESTION_SCHEMA,
173-
variables: { id: questionId },
174-
errorPolicy: "all",
175-
});
176-
if (!data?.question) {
177-
return { error: "無法取得題目資料", details: error?.message };
178-
}
179-
180-
return {
181-
schema: data.question.database.structure
182-
? {
183-
tables: data.question.database.structure.tables.map((table) => ({
184-
name: table.name,
185-
columns: table.columns,
186-
})),
187-
}
188-
: null,
189-
};
190-
},
191-
}),
192-
webSearch: anthropic.tools.webSearch_20250305({
193-
maxUses: 5,
194-
}),
195-
},
196-
});
197-
198-
return result.toUIMessageStreamResponse();
103+
const model = anthropic("claude-sonnet-4-20250514");
104+
const posthogClient = await createPostHogClient();
105+
106+
try {
107+
const tracedModel = withTracing(model, posthogClient, {
108+
posthogDistinctId: userInfo.sub,
109+
});
110+
111+
const result = streamText({
112+
model: tracedModel,
113+
providerOptions: {
114+
anthropic: {
115+
thinking: { type: "enabled", budgetTokens: 12000 },
116+
} satisfies AnthropicProviderOptions,
117+
},
118+
messages: convertToModelMessages(messages),
119+
system: preparedPrompt,
120+
stopWhen: stepCountIs(10),
121+
tools: {
122+
getMyAnswer: tool({
123+
description:
124+
"取得使用者最後提交的答案結果,包括查詢結果、錯誤訊息和狀態。如果使用者問關於他們的答案,使用這個工具。",
125+
inputSchema: z.object({}),
126+
execute: async () => {
127+
const { data, error } = await apollo.query({
128+
query: USER_ANSWER_RESULT,
129+
variables: { id: questionId },
130+
errorPolicy: "all",
131+
});
132+
if (!data?.question) {
133+
return { error: "無法取得題目資料", details: error?.message };
134+
}
135+
136+
const { lastSubmission } = data.question;
137+
if (!lastSubmission) {
138+
return { error: "使用者尚未提交答案" };
139+
}
140+
141+
return {
142+
status: lastSubmission.status,
143+
submittedCode: lastSubmission.submittedCode,
144+
queryResult: lastSubmission.queryResult
145+
? {
146+
columns: lastSubmission.queryResult.columns,
147+
rows: lastSubmission.queryResult.rows,
148+
}
149+
: null,
150+
error: lastSubmission.error,
151+
};
152+
},
153+
}),
154+
getCorrectAnswer: tool({
155+
description: "取得題目的正確答案結果,你可以對照和使用者的答案差異。",
156+
inputSchema: z.object({}),
157+
execute: async () => {
158+
const { data, error } = await apollo.query({
159+
query: CORRECT_ANSWER_RESULT,
160+
variables: { id: questionId },
161+
errorPolicy: "all",
162+
});
163+
if (!data?.question) {
164+
return { error: "無法取得題目資料", details: error?.message };
165+
}
166+
167+
return {
168+
queryResult: data.question.referenceAnswerResult
169+
? {
170+
columns: data.question.referenceAnswerResult.columns,
171+
rows: data.question.referenceAnswerResult.rows,
172+
}
173+
: null,
174+
};
175+
},
176+
}),
177+
getQuestionSchema: tool({
178+
description: "取得題目的資料庫結構,你可以用這個數據輔助了解 SQL 結構。",
179+
inputSchema: z.object({}),
180+
execute: async () => {
181+
const { data, error } = await apollo.query({
182+
query: QUESTION_SCHEMA,
183+
variables: { id: questionId },
184+
errorPolicy: "all",
185+
});
186+
if (!data?.question) {
187+
return { error: "無法取得題目資料", details: error?.message };
188+
}
189+
190+
return {
191+
schema: data.question.database.structure
192+
? {
193+
tables: data.question.database.structure.tables.map((table) => ({
194+
name: table.name,
195+
columns: table.columns,
196+
})),
197+
}
198+
: null,
199+
};
200+
},
201+
}),
202+
webSearch: anthropic.tools.webSearch_20250305({
203+
maxUses: 5,
204+
}),
205+
},
206+
});
207+
208+
return result.toUIMessageStreamResponse();
209+
} finally {
210+
await posthogClient.shutdown();
211+
}
199212
}
200213

201214
export const prompt =

lib/auth.rsc.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,28 @@ export async function redirectIfAuthenticated(): Promise<void> {
1717
redirect("/");
1818
}
1919

20-
export async function checkAuthorizedStatus(requiredScopes?: string[]): Promise<boolean> {
20+
export async function getAuthorizedUserInfo(requiredScopes?: string[]) {
2121
const token = await getAuthToken();
2222
if (!token) {
23-
return false;
23+
return null;
2424
}
2525

2626
const authStatus = await getAuthStatus(token);
2727

2828
if (!authStatus.loggedIn || !authStatus.introspectResult?.active) {
29-
return false;
29+
return null;
3030
}
3131

3232
// check if the token has the required scope
3333
if (requiredScopes) {
3434
for (const scope of requiredScopes) {
3535
if (authStatus.introspectResult?.scope.includes(scope)) {
36-
return true;
36+
return authStatus.introspectResult;
3737
}
3838
}
3939

40-
return false;
40+
return null;
4141
}
4242

43-
return true;
43+
return authStatus.introspectResult;
4444
}

lib/posthog.rsc.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use server";
2+
3+
import { PostHog } from 'posthog-node'
4+
5+
export async function createPostHogClient() {
6+
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
7+
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
8+
flushAt: 1,
9+
flushInterval: 0
10+
})
11+
12+
return posthogClient
13+
}

0 commit comments

Comments
 (0)