Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/sample-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "sample-app",
"version": "0.0.1",
"description": "Sample app for using Traceloop SDK",
"type": "module",
"scripts": {
"build": "tsc --build tsconfig.json",
"run:anthropic": "npm run build && node dist/src/sample_anthropic.js",
Expand Down Expand Up @@ -45,6 +46,7 @@
"run:mcp": "npm run build && node dist/src/sample_mcp.js",
"run:mcp:real": "npm run build && node dist/src/sample_mcp_real.js",
"run:mcp:working": "npm run build && node dist/src/sample_mcp_working.js",
"run:chatbot_interactive": "npm run build && node dist/src/sample_chatbot_interactive.js",
"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",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
Expand Down
284 changes: 284 additions & 0 deletions packages/sample-app/src/sample_chatbot_interactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import * as traceloop from "@traceloop/node-server-sdk";
import { openai } from "@ai-sdk/openai";
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
import * as readline from "readline";
import { z } from "zod";

import "dotenv/config";

traceloop.initialize({
appName: "sample_chatbot_interactive",
disableBatch: true,
});

const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
};

class InteractiveChatbot {
private conversationHistory: CoreMessage[] = [];
private rl: readline.Interface;
private sessionId: string;
private userId: string;

constructor() {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`,
});
this.sessionId = `session-${Date.now()}`;
this.userId = `user-${Math.random().toString(36).substring(7)}`;
}

@traceloop.task({ name: "summarize_interaction" })
async generateSummary(
userMessage: string,
assistantResponse: string,
): Promise<string> {
console.log(
`\n${colors.yellow}▼ SUMMARY${colors.reset} ${colors.dim}TASK${colors.reset}`,
);

const summaryResult = await streamText({
model: openai("gpt-4o-mini"),
messages: [
{
role: "system",
content:
"Create a very brief title (3-6 words) that summarizes this conversation exchange. Only return the title, nothing else.",
},
{
role: "user",
content: `User: ${userMessage}\n\nAssistant: ${assistantResponse}`,
},
],
experimental_telemetry: { isEnabled: true },
});

let summary = "";
for await (const chunk of summaryResult.textStream) {
summary += chunk;
}

const cleanSummary = summary.trim().replace(/^["']|["']$/g, "");
console.log(`${colors.dim}${cleanSummary}${colors.reset}`);

return cleanSummary;
}

@traceloop.workflow((thisArg) => {
const self = thisArg as InteractiveChatbot;
return {
name: "chat_interaction",
associationProperties: {
[traceloop.AssociationProperty.SESSION_ID]: self.sessionId,
[traceloop.AssociationProperty.USER_ID]: self.userId,
},
};
})
async processMessage(userMessage: string): Promise<string> {

// Add user message to history
this.conversationHistory.push({
role: "user",
content: userMessage,
});

console.log(`\n${colors.green}${colors.bright}Assistant: ${colors.reset}`);

// Stream the response
const result = await streamText({
model: openai("gpt-4o"),
messages: [
{
role: "system",
content:
"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.",
},
...this.conversationHistory,
],
tools: {
calculator: tool({
description:
"Perform mathematical calculations. Supports basic arithmetic operations.",
inputSchema: z.object({
expression: z
.string()
.describe(
"The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')",
),
}),
execute: async ({ expression }: { expression: string }) => {
try {
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, "");
const result = eval(sanitized);
console.log(
`\n${colors.yellow}🔧 Calculator: ${expression} = ${result}${colors.reset}`,
);
return { result, expression };
} catch (error) {
return { error: "Invalid mathematical expression" };
}
},
}),
getCurrentWeather: tool({
description:
"Get the current weather for a location. Use this when users ask about weather conditions.",
inputSchema: z.object({
location: z
.string()
.describe("The city and country, e.g., 'London, UK'"),
}),
execute: async ({ location }: { location: string }) => {
console.log(
`\n${colors.yellow}🔧 Weather: Checking weather for ${location}${colors.reset}`,
);
// Simulated weather data
const weatherConditions = [
"sunny",
"cloudy",
"rainy",
"partly cloudy",
];
const condition =
weatherConditions[
Math.floor(Math.random() * weatherConditions.length)
];
const temperature = Math.floor(Math.random() * 30) + 10; // 10-40°C
return {
location,
temperature: `${temperature}°C`,
condition,
humidity: `${Math.floor(Math.random() * 40) + 40}%`,
};
},
}),
getTime: tool({
description:
"Get the current date and time. Use this when users ask about the current time or date.",
inputSchema: z.object({
timezone: z
.string()
.optional()
.describe("Optional timezone (e.g., 'America/New_York')"),
}),
execute: async ({ timezone }: { timezone?: string }) => {
const now = new Date();
const options: Intl.DateTimeFormatOptions = {
timeZone: timezone,
dateStyle: "full",
timeStyle: "long",
};
const formatted = now.toLocaleString("en-US", options);
console.log(
`\n${colors.yellow}🔧 Time: ${formatted}${colors.reset}`,
);
return {
datetime: formatted,
timestamp: now.toISOString(),
timezone: timezone || "local",
};
},
}),
},
stopWhen: stepCountIs(5),
experimental_telemetry: { isEnabled: true },
});

let fullResponse = "";
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
fullResponse += chunk;
}

console.log("\n");

const finalResult = await result.response;


for (const message of finalResult.messages) {
this.conversationHistory.push(message);
}

await this.generateSummary(userMessage, fullResponse);

return fullResponse;
}

clearHistory(): void {
this.conversationHistory = [];
console.log(
`\n${colors.magenta}✓ Conversation history cleared${colors.reset}\n`,
);
}

async start(): Promise<void> {
console.log(
`${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`,
);
console.log(
`║ Interactive AI Chatbot with Traceloop ║`,
);
console.log(
`╚════════════════════════════════════════════════════════════╝${colors.reset}\n`,
);
console.log(
`${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`,
);
console.log(`${colors.dim}Session ID: ${this.sessionId}${colors.reset}`);
console.log(`${colors.dim}User ID: ${this.userId}${colors.reset}\n`);

this.rl.prompt();

this.rl.on("line", async (input: string) => {
const trimmedInput = input.trim();

if (!trimmedInput) {
this.rl.prompt();
return;
}

if (trimmedInput === "/exit") {
console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`);
this.rl.close();
process.exit(0);
}

if (trimmedInput === "/clear") {
this.clearHistory();
this.rl.prompt();
return;
}

try {
await this.processMessage(trimmedInput);
} catch (error) {
console.error(
`\n${colors.bright}Error:${colors.reset} ${error instanceof Error ? error.message : String(error)}\n`,
);
}

this.rl.prompt();
});

this.rl.on("close", () => {
console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`);
process.exit(0);
});
}
}

async function main() {
const chatbot = new InteractiveChatbot();
await chatbot.start();
}

main().catch(console.error);
31 changes: 31 additions & 0 deletions packages/traceloop-sdk/src/lib/associations/associations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Standard association properties for tracing.
* Use these with withAssociationProperties() or decorator associationProperties config.
*
* @example
* ```typescript
* // With withAssociationProperties
* await traceloop.withAssociationProperties(
* {
* [traceloop.AssociationProperty.USER_ID]: "12345",
* [traceloop.AssociationProperty.SESSION_ID]: "session-abc"
* },
* async () => {
* await chat();
* }
* );
*
* // With decorator
* @traceloop.workflow((thisArg) => ({
* name: "my_workflow",
* associationProperties: {
* [traceloop.AssociationProperty.USER_ID]: (thisArg as MyClass).userId,
* },
* }))
* ```
*/
export enum AssociationProperty {
CUSTOMER_ID = "customer_id",
USER_ID = "user_id",
SESSION_ID = "session_id",
}
1 change: 1 addition & 0 deletions packages/traceloop-sdk/src/lib/node-server-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@ export * from "./tracing/association";
export * from "./tracing/custom-metric";
export * from "./tracing/span-processor";
export * from "./prompts";
export { AssociationProperty } from "./associations/associations";

// Instrumentations are now initialized only when initialize() is called
8 changes: 6 additions & 2 deletions packages/traceloop-sdk/src/lib/tracing/span-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,14 @@ const onSpanStart = (span: Span): void => {
spanAgentNames.set(spanId, { agentName, timestamp: Date.now() });
}

// Check for association properties in context (set by decorators or withAssociationProperties)
const associationProperties = context
.active()
.getValue(ASSOCATION_PROPERTIES_KEY);
if (associationProperties) {
.getValue(ASSOCATION_PROPERTIES_KEY) as
| { [name: string]: string }
| undefined;

if (associationProperties && Object.keys(associationProperties).length > 0) {
for (const [key, value] of Object.entries(associationProperties)) {
span.setAttribute(
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,
Expand Down
Loading
Loading