Skip to content

Commit 7a42933

Browse files
authored
fix(tracing): Add association property (#852)
1 parent fa98799 commit 7a42933

File tree

6 files changed

+610
-2
lines changed

6 files changed

+610
-2
lines changed

packages/sample-app/package.json

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

packages/traceloop-sdk/src/lib/node-server-sdk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ export * from "./tracing/association";
7676
export * from "./tracing/custom-metric";
7777
export * from "./tracing/span-processor";
7878
export * from "./prompts";
79+
export { AssociationProperty } from "./associations/associations";
7980

8081
// Instrumentations are now initialized only when initialize() is called

packages/traceloop-sdk/src/lib/tracing/span-processor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,14 @@ const onSpanStart = (span: Span): void => {
195195
spanAgentNames.set(spanId, { agentName, timestamp: Date.now() });
196196
}
197197

198+
// Check for association properties in context (set by decorators or withAssociationProperties)
198199
const associationProperties = context
199200
.active()
200-
.getValue(ASSOCATION_PROPERTIES_KEY);
201-
if (associationProperties) {
201+
.getValue(ASSOCATION_PROPERTIES_KEY) as
202+
| { [name: string]: string }
203+
| undefined;
204+
205+
if (associationProperties && Object.keys(associationProperties).length > 0) {
202206
for (const [key, value] of Object.entries(associationProperties)) {
203207
span.setAttribute(
204208
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,

0 commit comments

Comments
 (0)