Skip to content

Commit 75cf2fe

Browse files
authored
fix(traceloop-sdk): Add conversation decorator (#883)
1 parent 7a42933 commit 75cf2fe

File tree

8 files changed

+1021
-7
lines changed

8 files changed

+1021
-7
lines changed

packages/sample-app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
"run:mcp": "npm run build && node dist/src/sample_mcp.js",
4747
"run:mcp:real": "npm run build && node dist/src/sample_mcp_real.js",
4848
"run:mcp:working": "npm run build && node dist/src/sample_mcp_working.js",
49-
"run:chatbot_interactive": "npm run build && node dist/src/sample_chatbot_interactive.js",
49+
"run:chatbot_interactive": "npm run build && node dist/src/conversations/sample_chatbot_interactive.js",
50+
"run:conversation_simple": "npm run build && node dist/src/conversations/sample_with_conversation.js",
51+
"run:conversation_id_config": "npm run build && node dist/src/conversations/sample_conversation_id_config.js",
5052
"dev:image_generation": "pnpm --filter @traceloop/instrumentation-openai build && pnpm --filter @traceloop/node-server-sdk build && npm run build && node dist/src/sample_openai_image_generation.js",
5153
"lint": "eslint .",
5254
"lint:fix": "eslint . --fix"

packages/sample-app/src/sample_chatbot_interactive.ts renamed to packages/sample-app/src/conversations/sample_chatbot_interactive.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const colors = {
2525
class InteractiveChatbot {
2626
private conversationHistory: CoreMessage[] = [];
2727
private rl: readline.Interface;
28-
private sessionId: string;
28+
private conversationId: string;
2929
private userId: string;
3030

3131
constructor() {
@@ -34,7 +34,7 @@ class InteractiveChatbot {
3434
output: process.stdout,
3535
prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`,
3636
});
37-
this.sessionId = `session-${Date.now()}`;
37+
this.conversationId = `conversation-${Date.now()}`;
3838
this.userId = `user-${Math.random().toString(36).substring(7)}`;
3939
}
4040

@@ -74,12 +74,14 @@ class InteractiveChatbot {
7474
return cleanSummary;
7575
}
7676

77+
@traceloop.conversation(
78+
(thisArg) => (thisArg as InteractiveChatbot).conversationId,
79+
)
7780
@traceloop.workflow((thisArg) => {
7881
const self = thisArg as InteractiveChatbot;
7982
return {
8083
name: "chat_interaction",
8184
associationProperties: {
82-
[traceloop.AssociationProperty.SESSION_ID]: self.sessionId,
8385
[traceloop.AssociationProperty.USER_ID]: self.userId,
8486
},
8587
};
@@ -231,7 +233,9 @@ class InteractiveChatbot {
231233
console.log(
232234
`${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`,
233235
);
234-
console.log(`${colors.dim}Session ID: ${this.sessionId}${colors.reset}`);
236+
console.log(
237+
`${colors.dim}Conversation ID: ${this.conversationId}${colors.reset}`,
238+
);
235239
console.log(`${colors.dim}User ID: ${this.userId}${colors.reset}\n`);
236240

237241
this.rl.prompt();
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import * as traceloop from "@traceloop/node-server-sdk";
2+
import { openai } from "@ai-sdk/openai";
3+
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
4+
import * as readline from "readline";
5+
import { z } from "zod";
6+
7+
import "dotenv/config";
8+
9+
// Example of using conversationId in DecoratorConfig instead of @conversation decorator
10+
// This is an alternative approach that doesn't require a separate decorator
11+
12+
traceloop.initialize({
13+
appName: "sample_conversation_id_config",
14+
disableBatch: true,
15+
});
16+
17+
const colors = {
18+
reset: "\x1b[0m",
19+
bright: "\x1b[1m",
20+
dim: "\x1b[2m",
21+
cyan: "\x1b[36m",
22+
green: "\x1b[32m",
23+
yellow: "\x1b[33m",
24+
blue: "\x1b[34m",
25+
magenta: "\x1b[35m",
26+
};
27+
28+
class InteractiveChatbot {
29+
private conversationHistory: CoreMessage[] = [];
30+
private rl: readline.Interface;
31+
private conversationId: string;
32+
private userId: string;
33+
34+
constructor() {
35+
this.rl = readline.createInterface({
36+
input: process.stdin,
37+
output: process.stdout,
38+
prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`,
39+
});
40+
this.conversationId = `conversation-${Date.now()}`;
41+
this.userId = `user-${Math.random().toString(36).substring(7)}`;
42+
}
43+
44+
@traceloop.task({ name: "summarize_interaction" })
45+
async generateSummary(
46+
userMessage: string,
47+
assistantResponse: string,
48+
): Promise<string> {
49+
console.log(
50+
`\n${colors.yellow}▼ SUMMARY${colors.reset} ${colors.dim}TASK${colors.reset}`,
51+
);
52+
53+
const summaryResult = await streamText({
54+
model: openai("gpt-4o-mini"),
55+
messages: [
56+
{
57+
role: "system",
58+
content:
59+
"Create a very brief title (3-6 words) that summarizes this conversation exchange. Only return the title, nothing else.",
60+
},
61+
{
62+
role: "user",
63+
content: `User: ${userMessage}\n\nAssistant: ${assistantResponse}`,
64+
},
65+
],
66+
experimental_telemetry: { isEnabled: true },
67+
});
68+
69+
let summary = "";
70+
for await (const chunk of summaryResult.textStream) {
71+
summary += chunk;
72+
}
73+
74+
const cleanSummary = summary.trim().replace(/^["']|["']$/g, "");
75+
console.log(`${colors.dim}${cleanSummary}${colors.reset}`);
76+
77+
return cleanSummary;
78+
}
79+
80+
// Using conversationId directly in the workflow config instead of @conversation decorator
81+
@traceloop.workflow((thisArg) => {
82+
const self = thisArg as InteractiveChatbot;
83+
return {
84+
name: "chat_interaction",
85+
conversationId: self.conversationId, // <-- Using conversationId in config
86+
associationProperties: {
87+
[traceloop.AssociationProperty.USER_ID]: self.userId,
88+
},
89+
};
90+
})
91+
async processMessage(userMessage: string): Promise<string> {
92+
// Add user message to history
93+
this.conversationHistory.push({
94+
role: "user",
95+
content: userMessage,
96+
});
97+
98+
console.log(`\n${colors.green}${colors.bright}Assistant: ${colors.reset}`);
99+
100+
// Stream the response
101+
const result = await streamText({
102+
model: openai("gpt-4o"),
103+
messages: [
104+
{
105+
role: "system",
106+
content:
107+
"You are a helpful AI assistant with access to tools. Use the available tools when appropriate to provide accurate information. Provide clear, concise, and friendly responses.",
108+
},
109+
...this.conversationHistory,
110+
],
111+
tools: {
112+
calculator: tool({
113+
description:
114+
"Perform mathematical calculations. Supports basic arithmetic operations.",
115+
inputSchema: z.object({
116+
expression: z
117+
.string()
118+
.describe(
119+
"The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')",
120+
),
121+
}),
122+
execute: async ({ expression }: { expression: string }) => {
123+
try {
124+
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, "");
125+
const result = eval(sanitized);
126+
console.log(
127+
`\n${colors.yellow}🔧 Calculator: ${expression} = ${result}${colors.reset}`,
128+
);
129+
return { result, expression };
130+
} catch (error) {
131+
return { error: "Invalid mathematical expression" };
132+
}
133+
},
134+
}),
135+
getCurrentWeather: tool({
136+
description:
137+
"Get the current weather for a location. Use this when users ask about weather conditions.",
138+
inputSchema: z.object({
139+
location: z
140+
.string()
141+
.describe("The city and country, e.g., 'London, UK'"),
142+
}),
143+
execute: async ({ location }: { location: string }) => {
144+
console.log(
145+
`\n${colors.yellow}🔧 Weather: Checking weather for ${location}${colors.reset}`,
146+
);
147+
// Simulated weather data
148+
const weatherConditions = [
149+
"sunny",
150+
"cloudy",
151+
"rainy",
152+
"partly cloudy",
153+
];
154+
const condition =
155+
weatherConditions[
156+
Math.floor(Math.random() * weatherConditions.length)
157+
];
158+
const temperature = Math.floor(Math.random() * 30) + 10; // 10-40°C
159+
return {
160+
location,
161+
temperature: `${temperature}°C`,
162+
condition,
163+
humidity: `${Math.floor(Math.random() * 40) + 40}%`,
164+
};
165+
},
166+
}),
167+
getTime: tool({
168+
description:
169+
"Get the current date and time. Use this when users ask about the current time or date.",
170+
inputSchema: z.object({
171+
timezone: z
172+
.string()
173+
.optional()
174+
.describe("Optional timezone (e.g., 'America/New_York')"),
175+
}),
176+
execute: async ({ timezone }: { timezone?: string }) => {
177+
const now = new Date();
178+
const options: Intl.DateTimeFormatOptions = {
179+
timeZone: timezone,
180+
dateStyle: "full",
181+
timeStyle: "long",
182+
};
183+
const formatted = now.toLocaleString("en-US", options);
184+
console.log(
185+
`\n${colors.yellow}🔧 Time: ${formatted}${colors.reset}`,
186+
);
187+
return {
188+
datetime: formatted,
189+
timestamp: now.toISOString(),
190+
timezone: timezone || "local",
191+
};
192+
},
193+
}),
194+
},
195+
stopWhen: stepCountIs(5),
196+
experimental_telemetry: { isEnabled: true },
197+
});
198+
199+
let fullResponse = "";
200+
for await (const chunk of result.textStream) {
201+
process.stdout.write(chunk);
202+
fullResponse += chunk;
203+
}
204+
205+
console.log("\n");
206+
207+
const finalResult = await result.response;
208+
209+
for (const message of finalResult.messages) {
210+
this.conversationHistory.push(message);
211+
}
212+
213+
await this.generateSummary(userMessage, fullResponse);
214+
215+
return fullResponse;
216+
}
217+
218+
clearHistory(): void {
219+
this.conversationHistory = [];
220+
console.log(
221+
`\n${colors.magenta}✓ Conversation history cleared${colors.reset}\n`,
222+
);
223+
}
224+
225+
async start(): Promise<void> {
226+
console.log(
227+
`${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`,
228+
);
229+
console.log(
230+
`║ Interactive AI Chatbot - Using conversationId Config ║`,
231+
);
232+
console.log(
233+
`╚════════════════════════════════════════════════════════════╝${colors.reset}\n`,
234+
);
235+
console.log(
236+
`${colors.dim}This example uses conversationId in DecoratorConfig${colors.reset}`,
237+
);
238+
console.log(
239+
`${colors.dim}instead of the @conversation decorator${colors.reset}\n`,
240+
);
241+
console.log(
242+
`${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`,
243+
);
244+
console.log(
245+
`${colors.dim}Conversation ID: ${this.conversationId}${colors.reset}`,
246+
);
247+
console.log(`${colors.dim}User ID: ${this.userId}${colors.reset}\n`);
248+
249+
this.rl.prompt();
250+
251+
this.rl.on("line", async (input: string) => {
252+
const trimmedInput = input.trim();
253+
254+
if (!trimmedInput) {
255+
this.rl.prompt();
256+
return;
257+
}
258+
259+
if (trimmedInput === "/exit") {
260+
console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`);
261+
this.rl.close();
262+
process.exit(0);
263+
}
264+
265+
if (trimmedInput === "/clear") {
266+
this.clearHistory();
267+
this.rl.prompt();
268+
return;
269+
}
270+
271+
try {
272+
await this.processMessage(trimmedInput);
273+
} catch (error) {
274+
console.error(
275+
`\n${colors.bright}Error:${colors.reset} ${error instanceof Error ? error.message : String(error)}\n`,
276+
);
277+
}
278+
279+
this.rl.prompt();
280+
});
281+
282+
this.rl.on("close", () => {
283+
console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`);
284+
process.exit(0);
285+
});
286+
}
287+
}
288+
289+
async function main() {
290+
const chatbot = new InteractiveChatbot();
291+
await chatbot.start();
292+
}
293+
294+
main().catch(console.error);

0 commit comments

Comments
 (0)