diff --git a/apps/webapp/app/v3/otlpExporter.server.ts b/apps/webapp/app/v3/otlpExporter.server.ts index 616bac9199..b3487968d2 100644 --- a/apps/webapp/app/v3/otlpExporter.server.ts +++ b/apps/webapp/app/v3/otlpExporter.server.ts @@ -27,6 +27,7 @@ import { import { logger } from "~/services/logger.server"; import { trace, Tracer } from "@opentelemetry/api"; import { startSpan } from "./tracing.server"; +import { enrichCreatableEvents } from "./utils/enrichCreatableEvents.server"; export type OTLPExporterConfig = { batchSize: number; @@ -54,14 +55,16 @@ class OTLPExporter { return convertSpansToCreateableEvents(resourceSpan); }); - this.#logEventsVerbose(events); + const enrichedEvents = enrichCreatableEvents(events); - span.setAttribute("event_count", events.length); + this.#logEventsVerbose(enrichedEvents); + + span.setAttribute("event_count", enrichedEvents.length); if (immediate) { - await this._eventRepository.insertManyImmediate(events); + await this._eventRepository.insertManyImmediate(enrichedEvents); } else { - await this._eventRepository.insertMany(events); + await this._eventRepository.insertMany(enrichedEvents); } return ExportTraceServiceResponse.create(); @@ -79,14 +82,16 @@ class OTLPExporter { return convertLogsToCreateableEvents(resourceLog); }); - this.#logEventsVerbose(events); + const enrichedEvents = enrichCreatableEvents(events); + + this.#logEventsVerbose(enrichedEvents); - span.setAttribute("event_count", events.length); + span.setAttribute("event_count", enrichedEvents.length); if (immediate) { - await this._eventRepository.insertManyImmediate(events); + await this._eventRepository.insertManyImmediate(enrichedEvents); } else { - await this._eventRepository.insertMany(events); + await this._eventRepository.insertMany(enrichedEvents); } return ExportLogsServiceResponse.create(); @@ -135,16 +140,28 @@ class OTLPExporter { (attribute) => attribute.key === SemanticInternalAttributes.TRIGGER ); - if (!triggerAttribute) { + const executionEnvironmentAttribute = resourceSpan.resource?.attributes.find( + (attribute) => attribute.key === SemanticInternalAttributes.EXECUTION_ENVIRONMENT + ); + + if (!triggerAttribute && !executionEnvironmentAttribute) { logger.debug("Skipping resource span without trigger attribute", { attributes: resourceSpan.resource?.attributes, spans: resourceSpan.scopeSpans.flatMap((scopeSpan) => scopeSpan.spans), }); - return; + return true; // go ahead and let this resource span through + } + + const executionEnvironment = isStringValue(executionEnvironmentAttribute?.value) + ? executionEnvironmentAttribute.value.stringValue + : undefined; + + if (executionEnvironment === "trigger") { + return true; // go ahead and let this resource span through } - return isBoolValue(triggerAttribute.value) ? triggerAttribute.value.boolValue : false; + return isBoolValue(triggerAttribute?.value) ? triggerAttribute.value.boolValue : false; }); } diff --git a/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts new file mode 100644 index 0000000000..39b2002dd9 --- /dev/null +++ b/apps/webapp/app/v3/utils/enrichCreatableEvents.server.ts @@ -0,0 +1,67 @@ +import type { CreatableEvent } from "../eventRepository.server"; + +export function enrichCreatableEvents(events: CreatableEvent[]) { + return events.map((event) => { + return enrichCreatableEvent(event); + }); +} + +function enrichCreatableEvent(event: CreatableEvent): CreatableEvent { + const message = formatPythonStyle(event.message, event.properties); + + event.message = message; + event.style = enrichStyle(event); + + return event; +} + +function enrichStyle(event: CreatableEvent) { + const baseStyle = event.style ?? {}; + const props = event.properties; + + // Direct property access and early returns + // GenAI System check + const system = props["gen_ai.system"]; + if (typeof system === "string") { + return { ...baseStyle, icon: `tabler-brand-${system}` }; + } + + // Agent workflow check + const name = props["name"]; + if (typeof name === "string" && name.includes("Agent workflow")) { + return { ...baseStyle, icon: "tabler-brain" }; + } + + return baseStyle; +} + +function repr(value: any): string { + if (typeof value === "string") { + return `'${value}'`; + } + return String(value); +} + +function formatPythonStyle(template: string, values: Record): string { + // Early return if template is too long + if (template.length >= 256) { + return template; + } + + // Early return if no template variables present + if (!template.includes("{")) { + return template; + } + + return template.replace(/\{([^}]+?)(?:!r)?\}/g, (match, key) => { + const hasRepr = match.endsWith("!r}"); + const actualKey = hasRepr ? key : key; + const value = values[actualKey]; + + if (value === undefined) { + return match; + } + + return hasRepr ? repr(value) : String(value); + }); +} diff --git a/apps/webapp/test/otlpExporter.test.ts b/apps/webapp/test/otlpExporter.test.ts new file mode 100644 index 0000000000..98380f2a59 --- /dev/null +++ b/apps/webapp/test/otlpExporter.test.ts @@ -0,0 +1,397 @@ +import { describe, it, expect } from "vitest"; +import { enrichCreatableEvents } from "../app/v3/utils/enrichCreatableEvents.server.js"; +import { + RuntimeEnvironmentType, + TaskEventKind, + TaskEventLevel, + TaskEventStatus, +} from "@trigger.dev/database"; +import { SemanticInternalAttributes } from "@trigger.dev/core/v3"; + +describe("OTLPExporter", () => { + describe("enrichCreatableEvents", () => { + it("should enrich creatable events that have template strings in the message", () => { + const events = [ + { + traceId: "4e184936f7a0ef5af428a29b4d47da9d", + spanId: "1be4ebd8c78e43e9", + parentId: "f1a6d77fd5872bbe", + message: "Responses API with {gen_ai.request.model!r}", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1743103947959190000), + links: [], + events: [], + duration: 2931702000, + properties: { + "code.filepath": "src/trigger/python/agent.py", + "code.lineno": 171, + model_settings: + '{"temperature":null,"top_p":null,"frequency_penalty":null,"presence_penalty":null,"tool_choice":null,"parallel_tool_calls":false,"truncation":null,"max_tokens":null}', + "gen_ai.request.model": "gpt-4o", + "logfire.msg_template": "Responses API with {gen_ai.request.model!r}", + "logfire.span_type": "span", + response_id: "resp_67e5a7cd046c8192b3bfb6c863057f60003e29fb266e3c7b", + "gen_ai.system": "openai", + "gen_ai.response.model": "gpt-4o-2024-08-06", + response: + '{"id":"resp_67e5a7cd046c8192b3bfb6c863057f60003e29fb266e3c7b","created_at":1743103949.0,"error":null,"incomplete_details":null,"instructions":"You are an expert at enriching contact information data.\\n Your task is to use web search to find additional information about a person\\n based on their basic contact details.\\n\\n Please find the basic information, company information, and social media information for the person.\\n\\n The input data is from an unspecified CSV dataset, but most likely a CSV dump from a CRM or other source like Mailchimp, etc.\\n\\n You\'ll receive the header row and a single row from the dataset to enrich, in their raw form.\\n \\n Only include information you are confident about from reliable sources.\\n Use null for fields where you cannot find reliable information.\\n ","metadata":{},"model":"gpt-4o-2024-08-06","object":"response","output":[{"arguments":"{\\"input\\":\\"Eric,eric@trigger.dev\\"}","call_id":"call_OPlvy1ubdx75L8AgGIfVTWlW","name":"basic_info_agent","type":"function_call","id":"fc_67e5a7ce1418819281d73995bf0ffcf9003e29fb266e3c7b","status":"completed"}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"basic_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"basic_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract basic information from a person\'s contact details"},{"name":"company_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"company_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract company information from a person\'s contact details"},{"name":"social_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"social_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract social media information from a person\'s contact details"}],"top_p":1.0,"max_output_tokens":null,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"status":"completed","text":{"format":{"schema_":{"$defs":{"BasicInfo":{"properties":{"name":{"description":"The name of the row","title":"Name","type":"string"},"email":{"description":"The email of the row","title":"Email","type":"string"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The first name of the row","title":"Firstname"},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The last name of the row","title":"Lastname"},"preferredName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The preferred name of the row","title":"Preferredname"}},"required":["name","email","firstName","lastName","preferredName"],"title":"BasicInfo","type":"object","additionalProperties":false},"CompanyInfo":{"properties":{"name":{"description":"The name of the company","title":"Name","type":"string"},"industry":{"description":"The industry of the company","title":"Industry","type":"string"}},"required":["name","industry"],"title":"CompanyInfo","type":"object","additionalProperties":false},"SocialInfo":{"properties":{"twitter":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Twitter username of the person","title":"Twitter"},"linkedin":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The LinkedIn username of the person","title":"Linkedin"},"facebook":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Facebook username of the person","title":"Facebook"},"instagram":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Instagram username of the person","title":"Instagram"}},"title":"SocialInfo","type":"object","additionalProperties":false,"required":["twitter","linkedin","facebook","instagram"]}},"properties":{"basicInfo":{"$ref":"#/$defs/BasicInfo"},"companyInfo":{"$ref":"#/$defs/CompanyInfo"},"socialInfo":{"$ref":"#/$defs/SocialInfo"}},"required":["basicInfo","companyInfo","socialInfo"],"title":"RowEnrichmentResult","type":"object","additionalProperties":false},"type":"json_schema","description":null,"name":"final_output","strict":true}},"truncation":"disabled","usage":{"input_tokens":702,"input_tokens_details":{"cached_tokens":0},"output_tokens":22,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":724},"user":null,"store":true}', + "gen_ai.operation.name": "chat", + raw_input: + '[{"content":"\\n Header row: Name,Email\\n Row to enrich: Eric,eric@trigger.dev\\n CSV URL: https://example.com/import.csv\\n ","role":"user"}]', + events: + '[{"event.name":"gen_ai.system.message","content":"You are an expert at enriching contact information data.\\n Your task is to use web search to find additional information about a person\\n based on their basic contact details.\\n\\n Please find the basic information, company information, and social media information for the person.\\n\\n The input data is from an unspecified CSV dataset, but most likely a CSV dump from a CRM or other source like Mailchimp, etc.\\n\\n You\'ll receive the header row and a single row from the dataset to enrich, in their raw form.\\n \\n Only include information you are confident about from reliable sources.\\n Use null for fields where you cannot find reliable information.\\n ","role":"system"},{"event.name":"gen_ai.user.message","content":"\\n Header row: Name,Email\\n Row to enrich: Eric,eric@trigger.dev\\n CSV URL: https://example.com/import.csv\\n ","role":"user"},{"event.name":"gen_ai.choice","index":0,"message":{"role":"assistant","tool_calls":[{"id":"call_OPlvy1ubdx75L8AgGIfVTWlW","type":"function","function":{"name":"basic_info_agent","arguments":"{\\"input\\":\\"Eric,eric@trigger.dev\\"}"}}]}}]', + "gen_ai.usage.input_tokens": 702, + "gen_ai.usage.output_tokens": 22, + "logfire.msg": "Responses API with 'gpt-4o'", + }, + payloadType: "application/json", + metadata: { + "service.instance.id": "eae3f05bfa924058b1aa79c6decd79fe", + }, + serviceName: "d3-demo", + serviceNamespace: "unknown", + environmentId: "cm8r871mw0002dy8n6ec3y0i2", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + organizationId: "cm5ohng6o0002dyuzvr0qq4xo", + projectId: "cm8r871lq0000dy8nhljain0w", + projectRef: "proj_wkbbtayxrmeyqhankehb", + runId: "run_cm8rr2zw50000efdyyq0376m5", + runIsTest: false, + attemptId: "deprecated", + taskSlug: "row-agent-runner", + taskPath: "src/trigger/d3Demo.ts", + taskExportName: "@deprecated", + workerId: "unmanaged", + workerVersion: "20250327.54", + queueId: "queue_pn4256pg6c2wc9rro3o7i", + queueName: "task/row-agent-runner", + batchId: "undefined", + idempotencyKey: "undefined", + machinePreset: "small-1x", + style: {}, + output: {}, + payload: {}, + }, + ]; + + const $events = enrichCreatableEvents(events); + expect($events).toBeDefined(); + + const event = $events[0]; + expect(event.message).toBe("Responses API with 'gpt-4o'"); + expect(event.style).toEqual({ + icon: "tabler-brand-openai", + }); + }); + + it("should enrich creatable events that have template strings without the !r in the message", () => { + const events = [ + { + traceId: "4e184936f7a0ef5af428a29b4d47da9d", + spanId: "1be4ebd8c78e43e9", + parentId: "f1a6d77fd5872bbe", + message: "Responses API with {gen_ai.request.model}", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1743103947959190000), + links: [], + events: [], + duration: 2931702000, + properties: { + "code.filepath": "src/trigger/python/agent.py", + "code.lineno": 171, + model_settings: + '{"temperature":null,"top_p":null,"frequency_penalty":null,"presence_penalty":null,"tool_choice":null,"parallel_tool_calls":false,"truncation":null,"max_tokens":null}', + "gen_ai.request.model": "gpt-4o", + "logfire.msg_template": "Responses API with {gen_ai.request.model}", + "logfire.span_type": "span", + response_id: "resp_67e5a7cd046c8192b3bfb6c863057f60003e29fb266e3c7b", + "gen_ai.system": "openai", + "gen_ai.response.model": "gpt-4o-2024-08-06", + response: + '{"id":"resp_67e5a7cd046c8192b3bfb6c863057f60003e29fb266e3c7b","created_at":1743103949.0,"error":null,"incomplete_details":null,"instructions":"You are an expert at enriching contact information data.\\n Your task is to use web search to find additional information about a person\\n based on their basic contact details.\\n\\n Please find the basic information, company information, and social media information for the person.\\n\\n The input data is from an unspecified CSV dataset, but most likely a CSV dump from a CRM or other source like Mailchimp, etc.\\n\\n You\'ll receive the header row and a single row from the dataset to enrich, in their raw form.\\n \\n Only include information you are confident about from reliable sources.\\n Use null for fields where you cannot find reliable information.\\n ","metadata":{},"model":"gpt-4o-2024-08-06","object":"response","output":[{"arguments":"{\\"input\\":\\"Eric,eric@trigger.dev\\"}","call_id":"call_OPlvy1ubdx75L8AgGIfVTWlW","name":"basic_info_agent","type":"function_call","id":"fc_67e5a7ce1418819281d73995bf0ffcf9003e29fb266e3c7b","status":"completed"}],"parallel_tool_calls":false,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"basic_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"basic_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract basic information from a person\'s contact details"},{"name":"company_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"company_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract company information from a person\'s contact details"},{"name":"social_info_agent","parameters":{"properties":{"input":{"title":"Input","type":"string"}},"required":["input"],"title":"social_info_agent_args","type":"object","additionalProperties":false},"strict":true,"type":"function","description":"Extract social media information from a person\'s contact details"}],"top_p":1.0,"max_output_tokens":null,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"status":"completed","text":{"format":{"schema_":{"$defs":{"BasicInfo":{"properties":{"name":{"description":"The name of the row","title":"Name","type":"string"},"email":{"description":"The email of the row","title":"Email","type":"string"},"firstName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The first name of the row","title":"Firstname"},"lastName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The last name of the row","title":"Lastname"},"preferredName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The preferred name of the row","title":"Preferredname"}},"required":["name","email","firstName","lastName","preferredName"],"title":"BasicInfo","type":"object","additionalProperties":false},"CompanyInfo":{"properties":{"name":{"description":"The name of the company","title":"Name","type":"string"},"industry":{"description":"The industry of the company","title":"Industry","type":"string"}},"required":["name","industry"],"title":"CompanyInfo","type":"object","additionalProperties":false},"SocialInfo":{"properties":{"twitter":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Twitter username of the person","title":"Twitter"},"linkedin":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The LinkedIn username of the person","title":"Linkedin"},"facebook":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Facebook username of the person","title":"Facebook"},"instagram":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The Instagram username of the person","title":"Instagram"}},"title":"SocialInfo","type":"object","additionalProperties":false,"required":["twitter","linkedin","facebook","instagram"]}},"properties":{"basicInfo":{"$ref":"#/$defs/BasicInfo"},"companyInfo":{"$ref":"#/$defs/CompanyInfo"},"socialInfo":{"$ref":"#/$defs/SocialInfo"}},"required":["basicInfo","companyInfo","socialInfo"],"title":"RowEnrichmentResult","type":"object","additionalProperties":false},"type":"json_schema","description":null,"name":"final_output","strict":true}},"truncation":"disabled","usage":{"input_tokens":702,"input_tokens_details":{"cached_tokens":0},"output_tokens":22,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":724},"user":null,"store":true}', + "gen_ai.operation.name": "chat", + raw_input: + '[{"content":"\\n Header row: Name,Email\\n Row to enrich: Eric,eric@trigger.dev\\n CSV URL: https://example.com/import.csv\\n ","role":"user"}]', + events: + '[{"event.name":"gen_ai.system.message","content":"You are an expert at enriching contact information data.\\n Your task is to use web search to find additional information about a person\\n based on their basic contact details.\\n\\n Please find the basic information, company information, and social media information for the person.\\n\\n The input data is from an unspecified CSV dataset, but most likely a CSV dump from a CRM or other source like Mailchimp, etc.\\n\\n You\'ll receive the header row and a single row from the dataset to enrich, in their raw form.\\n \\n Only include information you are confident about from reliable sources.\\n Use null for fields where you cannot find reliable information.\\n ","role":"system"},{"event.name":"gen_ai.user.message","content":"\\n Header row: Name,Email\\n Row to enrich: Eric,eric@trigger.dev\\n CSV URL: https://example.com/import.csv\\n ","role":"user"},{"event.name":"gen_ai.choice","index":0,"message":{"role":"assistant","tool_calls":[{"id":"call_OPlvy1ubdx75L8AgGIfVTWlW","type":"function","function":{"name":"basic_info_agent","arguments":"{\\"input\\":\\"Eric,eric@trigger.dev\\"}"}}]}}]', + "gen_ai.usage.input_tokens": 702, + "gen_ai.usage.output_tokens": 22, + "logfire.msg": "Responses API with 'gpt-4o'", + }, + payloadType: "application/json", + metadata: { + "service.instance.id": "eae3f05bfa924058b1aa79c6decd79fe", + }, + serviceName: "d3-demo", + serviceNamespace: "unknown", + environmentId: "cm8r871mw0002dy8n6ec3y0i2", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + organizationId: "cm5ohng6o0002dyuzvr0qq4xo", + projectId: "cm8r871lq0000dy8nhljain0w", + projectRef: "proj_wkbbtayxrmeyqhankehb", + runId: "run_cm8rr2zw50000efdyyq0376m5", + runIsTest: false, + attemptId: "deprecated", + taskSlug: "row-agent-runner", + taskPath: "src/trigger/d3Demo.ts", + taskExportName: "@deprecated", + workerId: "unmanaged", + workerVersion: "20250327.54", + queueId: "queue_pn4256pg6c2wc9rro3o7i", + queueName: "task/row-agent-runner", + batchId: "undefined", + idempotencyKey: "undefined", + machinePreset: "small-1x", + style: {}, + output: {}, + payload: {}, + }, + ]; + + const $events = enrichCreatableEvents(events); + expect($events).toBeDefined(); + + const event = $events[0]; + expect(event.message).toBe("Responses API with gpt-4o"); + expect(event.style).toEqual({ + icon: "tabler-brand-openai", + }); + }); + + it("should handle missing properties gracefully", () => { + const events = [ + { + message: "Responses API with {missing.property!r}", + properties: {}, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: {}, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].message).toBe("Responses API with {missing.property!r}"); + expect($events[0].style).toEqual({}); + }); + + it("should handle multiple template variables in one message", () => { + const events = [ + { + message: "Using {gen_ai.request.model!r} with temperature {gen_ai.request.temperature}", + properties: { + "gen_ai.request.model": "gpt-4", + "gen_ai.request.temperature": 0.7, + "gen_ai.system": "openai", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: {}, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].message).toBe("Using 'gpt-4' with temperature 0.7"); + expect($events[0].style).toEqual({ + icon: "tabler-brand-openai", + }); + }); + + it("should handle non-string property values", () => { + const events = [ + { + message: "Count is {count!r} and enabled is {enabled!r}", + properties: { + count: 42, + enabled: true, + "gen_ai.system": "anthropic", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: {}, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].message).toBe("Count is 42 and enabled is true"); + expect($events[0].style).toEqual({ + icon: "tabler-brand-anthropic", + }); + }); + + it("should handle messages without any template variables", () => { + const events = [ + { + message: "Plain message without variables", + properties: { + "gen_ai.system": "openai", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: {}, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].message).toBe("Plain message without variables"); + expect($events[0].style).toEqual({ + icon: "tabler-brand-openai", + }); + }); + + it("should handle style enrichment without gen_ai.system", () => { + const events = [ + { + message: "Message without system", + properties: { + "some.other.property": "value", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: { existingStyle: "value" }, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].style).toEqual({ existingStyle: "value" }); + }); + + it("should apply the first matching style enricher", () => { + const events = [ + { + message: "Agent workflow", + properties: { + "gen_ai.system": "openai", + name: "Agent workflow", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, // This would match the error enricher, but GenAI comes first + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: { existingStyle: "value" }, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].style).toEqual({ + existingStyle: "value", + icon: "tabler-brand-openai", // GenAI enricher wins because it's first + }); + }); + + it("should fall back to next enricher if first doesn't match", () => { + const events = [ + { + message: "Agent workflow", + properties: { + "task.type": "background", + name: "Agent workflow", + }, + traceId: "test", + spanId: "test", + isPartial: false, + isError: true, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: { existingStyle: "value" }, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].style).toEqual({ + existingStyle: "value", + icon: "tabler-brain", + }); + }); + + it("should preserve existing style if no enricher matches", () => { + const events = [ + { + message: "Test message", + properties: {}, + traceId: "test", + spanId: "test", + isPartial: false, + isError: false, + kind: TaskEventKind.INTERNAL, + level: TaskEventLevel.TRACE, + status: TaskEventStatus.UNSET, + startTime: BigInt(1), + style: { existingStyle: "value" }, + serviceName: "test", + environmentType: RuntimeEnvironmentType.DEVELOPMENT, + }, + ]; + + // @ts-expect-error + const $events = enrichCreatableEvents(events); + expect($events[0].style).toEqual({ + existingStyle: "value", + }); + }); + }); +}); diff --git a/packages/core/src/v3/otel/utils.ts b/packages/core/src/v3/otel/utils.ts index b5ba36aea5..8ce83549e7 100644 --- a/packages/core/src/v3/otel/utils.ts +++ b/packages/core/src/v3/otel/utils.ts @@ -1,4 +1,4 @@ -import { type Span, SpanStatusCode } from "@opentelemetry/api"; +import { type Span, SpanStatusCode, context, propagation } from "@opentelemetry/api"; export function recordSpanException(span: Span, error: unknown) { if (error instanceof Error) { @@ -20,3 +20,10 @@ function sanitizeSpanError(error: Error) { return sanitizedError; } + +export function carrierFromContext(): Record { + const carrier = {}; + propagation.inject(context.active(), carrier); + + return carrier; +} diff --git a/packages/core/src/v3/semanticInternalAttributes.ts b/packages/core/src/v3/semanticInternalAttributes.ts index 4cfc03e8ec..c23d1c2ef8 100644 --- a/packages/core/src/v3/semanticInternalAttributes.ts +++ b/packages/core/src/v3/semanticInternalAttributes.ts @@ -57,4 +57,5 @@ export const SemanticInternalAttributes = { RATE_LIMIT_RESET: "response.rateLimit.reset", SPAN_ATTEMPT: "$span.attempt", METRIC_EVENTS: "$metrics.events", + EXECUTION_ENVIRONMENT: "exec_env", }; diff --git a/packages/core/src/v3/workers/index.ts b/packages/core/src/v3/workers/index.ts index 7d67a23836..39c166823b 100644 --- a/packages/core/src/v3/workers/index.ts +++ b/packages/core/src/v3/workers/index.ts @@ -4,7 +4,12 @@ export { PreciseWallClock as DurableClock } from "../clock/preciseWallClock.js"; export { getEnvVar, getNumberEnvVar } from "../utils/getEnv.js"; export { OtelTaskLogger, logLevels } from "../logger/taskLogger.js"; export { ConsoleInterceptor } from "../consoleInterceptor.js"; -export { TracingSDK, type TracingDiagnosticLogLevel, recordSpanException } from "../otel/index.js"; +export { + TracingSDK, + type TracingDiagnosticLogLevel, + recordSpanException, + carrierFromContext, +} from "../otel/index.js"; export { StandardResourceCatalog } from "../resource-catalog/standardResourceCatalog.js"; export { TaskContextSpanProcessor, diff --git a/packages/python/src/index.ts b/packages/python/src/index.ts index f152cc01e5..6924802fd8 100644 --- a/packages/python/src/index.ts +++ b/packages/python/src/index.ts @@ -2,8 +2,10 @@ import { AsyncIterableStream, createAsyncIterableStreamFromAsyncIterable, SemanticInternalAttributes, + taskContext, } from "@trigger.dev/core/v3"; import { logger } from "@trigger.dev/sdk/v3"; +import { carrierFromContext } from "@trigger.dev/core/v3/otel"; import assert from "node:assert"; import fs from "node:fs"; import { Result, x, Options as XOptions } from "tinyexec"; @@ -17,6 +19,8 @@ export const python = { async run(scriptArgs: string[] = [], options: PythonExecOptions = {}): Promise { const pythonBin = process.env.PYTHON_BIN_PATH || "python"; + const carrier = carrierFromContext(); + return await logger.trace( "python.run()", async (span) => { @@ -27,6 +31,12 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, }, }, throwOnError: false, // Ensure errors are handled manually @@ -50,7 +60,7 @@ export const python = { attributes: { pythonBin, args: scriptArgs.join(" "), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, } ); @@ -69,6 +79,8 @@ export const python = { async (span) => { span.setAttribute("scriptPath", scriptPath); + const carrier = carrierFromContext(); + const result = await x( process.env.PYTHON_BIN_PATH || "python", [scriptPath, ...scriptArgs], @@ -79,6 +91,13 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, + OTEL_LOG_LEVEL: "DEBUG", }, }, throwOnError: false, @@ -93,7 +112,7 @@ export const python = { throw new Error( `${scriptPath} ${scriptArgs.join(" ")} exited with a non-zero code ${ result.exitCode - }:\n${result.stderr}` + }:\n${result.stdout}\n${result.stderr}` ); } @@ -104,7 +123,7 @@ export const python = { pythonBin: process.env.PYTHON_BIN_PATH || "python", scriptPath, args: scriptArgs.join(" "), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, } ); @@ -124,6 +143,8 @@ export const python = { async (tempFilePath) => { span.setAttribute("tempFilePath", tempFilePath); + const carrier = carrierFromContext(); + const pythonBin = process.env.PYTHON_BIN_PATH || "python"; const result = await x(pythonBin, [tempFilePath], { ...options, @@ -132,6 +153,12 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, }, }, throwOnError: false, @@ -157,7 +184,7 @@ export const python = { pythonBin: process.env.PYTHON_BIN_PATH || "python", contentPreview: scriptContent.substring(0, 100) + (scriptContent.length > 100 ? "..." : ""), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, } ); @@ -167,6 +194,8 @@ export const python = { run(scriptArgs: string[] = [], options: PythonExecOptions = {}): AsyncIterableStream { const pythonBin = process.env.PYTHON_BIN_PATH || "python"; + const carrier = carrierFromContext(); + const pythonProcess = x(pythonBin, scriptArgs, { ...options, nodeOptions: { @@ -174,6 +203,12 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, }, }, throwOnError: false, @@ -183,7 +218,7 @@ export const python = { attributes: { pythonBin, args: scriptArgs.join(" "), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, }); @@ -206,6 +241,8 @@ export const python = { const pythonBin = process.env.PYTHON_BIN_PATH || "python"; + const carrier = carrierFromContext(); + const pythonProcess = x(pythonBin, [scriptPath, ...scriptArgs], { ...options, nodeOptions: { @@ -213,6 +250,12 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, }, }, throwOnError: false, @@ -223,7 +266,7 @@ export const python = { pythonBin, scriptPath, args: scriptArgs.join(" "), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, }); @@ -243,6 +286,8 @@ export const python = { const pythonScriptPath = createTempFileSync(`script_${Date.now()}.py`, scriptContent); + const carrier = carrierFromContext(); + const pythonProcess = x(pythonBin, [pythonScriptPath], { ...options, nodeOptions: { @@ -250,6 +295,12 @@ export const python = { env: { ...process.env, ...options.env, + TRACEPARENT: carrier["traceparent"], + OTEL_RESOURCE_ATTRIBUTES: `${ + SemanticInternalAttributes.EXECUTION_ENVIRONMENT + }=trigger,${Object.entries(taskContext.attributes) + .map(([key, value]) => `${key}=${value}`) + .join(",")}`, }, }, throwOnError: false, @@ -260,7 +311,7 @@ export const python = { pythonBin, contentPreview: scriptContent.substring(0, 100) + (scriptContent.length > 100 ? "..." : ""), - [SemanticInternalAttributes.STYLE_ICON]: "brand-python", + [SemanticInternalAttributes.STYLE_ICON]: "python", }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfd146dbab..29f8450fc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1718,11 +1718,27 @@ importers: specifier: 3.23.8 version: 3.23.8 - references/agent-loop: + references/bun-catalog: + dependencies: + '@trigger.dev/sdk': + specifier: workspace:* + version: link:../../packages/trigger-sdk + devDependencies: + '@types/bun': + specifier: ^1.1.6 + version: 1.1.6 + trigger.dev: + specifier: workspace:* + version: link:../../packages/cli-v3 + + references/d3-demo: dependencies: '@ai-sdk/openai': specifier: 1.3.3 version: 1.3.3(zod@3.23.8) + '@trigger.dev/python': + specifier: workspace:* + version: link:../../packages/python '@trigger.dev/react-hooks': specifier: workspace:* version: link:../../packages/react-hooks @@ -1759,10 +1775,16 @@ importers: zod: specifier: 3.23.8 version: 3.23.8 + zod-to-json-schema: + specifier: ^3.24.5 + version: 3.24.5(zod@3.23.8) devDependencies: '@tailwindcss/postcss': specifier: ^4 version: 4.0.17 + '@trigger.dev/build': + specifier: workspace:* + version: link:../../packages/build '@types/node': specifier: ^20 version: 20.14.14 @@ -1782,19 +1804,6 @@ importers: specifier: ^5 version: 5.5.4 - references/bun-catalog: - dependencies: - '@trigger.dev/sdk': - specifier: workspace:* - version: link:../../packages/trigger-sdk - devDependencies: - '@types/bun': - specifier: ^1.1.6 - version: 1.1.6 - trigger.dev: - specifier: workspace:* - version: link:../../packages/cli-v3 - references/hello-world: dependencies: '@trigger.dev/sdk': @@ -2251,7 +2260,7 @@ packages: dependencies: '@ai-sdk/provider': 1.0.0 eventsource-parser: 3.0.0 - nanoid: 3.3.7 + nanoid: 3.3.11 secure-json-parse: 2.7.0 zod: 3.23.8 dev: true @@ -2487,7 +2496,7 @@ packages: json-schema: 0.4.0 secure-json-parse: 2.7.0 zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) dev: true /@ai-sdk/ui-utils@1.0.0(zod@3.23.8): @@ -2502,7 +2511,7 @@ packages: '@ai-sdk/provider': 1.0.0 '@ai-sdk/provider-utils': 2.0.0(zod@3.23.8) zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) dev: false /@ai-sdk/ui-utils@1.0.1(zod@3.23.8): @@ -2517,7 +2526,7 @@ packages: '@ai-sdk/provider': 1.0.0 '@ai-sdk/provider-utils': 2.0.1(zod@3.23.8) zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) dev: true /@ai-sdk/ui-utils@1.2.1(zod@3.23.8): @@ -2529,7 +2538,7 @@ packages: '@ai-sdk/provider': 1.1.0 '@ai-sdk/provider-utils': 2.2.1(zod@3.23.8) zod: 3.23.8 - zod-to-json-schema: 3.24.3(zod@3.23.8) + zod-to-json-schema: 3.24.5(zod@3.23.8) dev: false /@ai-sdk/vue@0.0.45(vue@3.4.38)(zod@3.23.8): @@ -8413,20 +8422,20 @@ packages: /@internationalized/date@3.5.5: resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@internationalized/message@3.1.1: resolution: {integrity: sha512-ZgHxf5HAPIaR0th+w0RUD62yF6vxitjlprSxmLJ1tam7FOekqRSDELMg4Cr/DdszG5YLsp5BG3FgHgqquQZbqw==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 intl-messageformat: 10.5.8 dev: false /@internationalized/message@3.1.4: resolution: {integrity: sha512-Dygi9hH1s7V9nha07pggCkvmRfDd3q2lWnMGvrJyrOwYMe1yj4D2T9BoH9I6MGR7xz0biQrtLPsqUkqXzIrBOw==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 intl-messageformat: 10.5.8 dev: false @@ -8439,7 +8448,7 @@ packages: /@internationalized/number@3.5.3: resolution: {integrity: sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@internationalized/string@3.2.0: @@ -8451,7 +8460,7 @@ packages: /@internationalized/string@3.2.3: resolution: {integrity: sha512-9kpfLoA8HegiWTeCbR2livhdVeKobCnVv8tlJ6M2jF+4tcMqDo94ezwlnrUANBWPgd8U7OXIHCk2Ov2qhk4KXw==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@ioredis/commands@1.2.0: @@ -12541,7 +12550,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/breadcrumbs': 3.7.7(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -12585,7 +12594,7 @@ packages: '@react-stately/toggle': 3.7.7(react@18.2.0) '@react-types/button': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -12604,7 +12613,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/calendar': 3.4.9(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12662,7 +12671,7 @@ packages: '@react-stately/toggle': 3.7.7(react@18.2.0) '@react-types/checkbox': 3.8.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -12686,7 +12695,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/combobox': 3.12.1(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12739,7 +12748,7 @@ packages: '@react-types/datepicker': 3.8.2(react@18.2.0) '@react-types/dialog': 3.5.12(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12799,7 +12808,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/dialog': 3.5.12(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12839,7 +12848,7 @@ packages: '@react-stately/dnd': 3.4.2(react@18.2.0) '@react-types/button': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12865,7 +12874,7 @@ packages: '@react-aria/interactions': 3.22.2(react@18.2.0) '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 clsx: 2.1.1 react: 18.2.0 dev: false @@ -12892,7 +12901,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-stately/form': 3.0.5(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -12914,7 +12923,7 @@ packages: '@react-types/checkbox': 3.8.3(react@18.2.0) '@react-types/grid': 3.2.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12938,7 +12947,7 @@ packages: '@react-types/checkbox': 3.6.0(react@18.2.0) '@react-types/grid': 3.2.3(react@18.2.0) '@react-types/shared': 3.22.0(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12978,7 +12987,7 @@ packages: '@react-stately/list': 3.10.8(react@18.2.0) '@react-stately/tree': 3.8.4(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13011,7 +13020,7 @@ packages: '@react-aria/ssr': 3.9.5(react@18.2.0) '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13035,7 +13044,7 @@ packages: '@react-aria/ssr': 3.9.5(react@18.2.0) '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13046,7 +13055,7 @@ packages: dependencies: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13085,7 +13094,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/link': 3.5.7(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13122,7 +13131,7 @@ packages: '@react-stately/list': 3.10.8(react@18.2.0) '@react-types/listbox': 3.5.1(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13130,13 +13139,13 @@ packages: /@react-aria/live-announcer@3.3.1: resolution: {integrity: sha512-hsc77U7S16trM86d+peqJCOCQ7/smO1cybgdpOuzXyiwcHQw8RQ4GrXrS37P4Ux/44E9nMZkOwATQRT2aK8+Ew==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@react-aria/live-announcer@3.3.4: resolution: {integrity: sha512-w8lxs35QrRrn6pBNzVfyGOeqWdxeVKf9U6bXIVwhq7rrTqRULL8jqy8RJIMfIs1s8G5FpwWYjyBOjl2g5Cu1iA==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@react-aria/menu@3.12.0(react-dom@18.2.0)(react@18.2.0): @@ -13180,7 +13189,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/menu': 3.9.11(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13193,7 +13202,7 @@ packages: '@react-aria/progress': 3.4.16(react@18.2.0) '@react-types/meter': 3.4.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13246,7 +13255,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/numberfield': 3.8.5(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13288,7 +13297,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/overlays': 3.8.9(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13303,7 +13312,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/progress': 3.5.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13353,7 +13362,7 @@ packages: '@react-stately/radio': 3.10.7(react@18.2.0) '@react-types/radio': 3.8.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13385,7 +13394,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/searchfield': 3.5.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13432,7 +13441,7 @@ packages: '@react-types/button': 3.9.6(react@18.2.0) '@react-types/select': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13466,7 +13475,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-stately/selection': 3.16.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13489,7 +13498,7 @@ packages: dependencies: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13506,7 +13515,7 @@ packages: '@react-stately/slider': 3.5.7(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/slider': 3.7.5(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13554,7 +13563,7 @@ packages: '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/button': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13575,7 +13584,7 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13600,7 +13609,7 @@ packages: '@react-stately/toggle': 3.7.7(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/switch': 3.5.5(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13650,7 +13659,7 @@ packages: '@react-types/grid': 3.2.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/table': 3.10.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13686,7 +13695,7 @@ packages: '@react-stately/tabs': 3.6.9(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/tabs': 3.3.9(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13726,7 +13735,7 @@ packages: '@react-stately/list': 3.10.8(react@18.2.0) '@react-types/button': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -13761,7 +13770,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/textfield': 3.9.6(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13775,7 +13784,7 @@ packages: '@react-aria/utils': 3.23.0(react@18.2.0) '@react-stately/toggle': 3.7.0(react@18.2.0) '@react-types/checkbox': 3.6.0(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13790,7 +13799,7 @@ packages: '@react-stately/toggle': 3.7.7(react@18.2.0) '@react-types/checkbox': 3.8.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13820,7 +13829,7 @@ packages: '@react-stately/tooltip': 3.4.12(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/tooltip': 3.4.11(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -13845,7 +13854,7 @@ packages: '@react-aria/ssr': 3.9.5(react@18.2.0) '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 clsx: 2.1.1 react: 18.2.0 dev: false @@ -13858,7 +13867,7 @@ packages: '@react-aria/interactions': 3.22.2(react@18.2.0) '@react-aria/utils': 3.25.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14546,7 +14555,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/calendar': 3.4.9(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14572,7 +14581,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/checkbox': 3.8.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14592,7 +14601,7 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 dependencies: '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14626,7 +14635,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/combobox': 3.12.1(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14652,7 +14661,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/datepicker': 3.8.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14690,7 +14699,7 @@ packages: dependencies: '@react-stately/selection': 3.16.2(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14703,7 +14712,7 @@ packages: /@react-stately/flags@3.0.3: resolution: {integrity: sha512-/ha7XFA0RZTQsbzSPwu3KkbNMgbvuM0GuMTYLTBWpgBrovBNTM+QqI/PfZTdHg8PwCYF4H5Y8gjdSpdulCvJFw==} dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 dev: false /@react-stately/form@3.0.0(react@18.2.0): @@ -14722,7 +14731,7 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 dependencies: '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14735,7 +14744,7 @@ packages: '@react-stately/selection': 3.14.2(react@18.2.0) '@react-types/grid': 3.2.3(react@18.2.0) '@react-types/shared': 3.22.0(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14748,7 +14757,7 @@ packages: '@react-stately/selection': 3.16.2(react@18.2.0) '@react-types/grid': 3.2.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14774,7 +14783,7 @@ packages: '@react-stately/selection': 3.16.2(react@18.2.0) '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14798,7 +14807,7 @@ packages: '@react-stately/overlays': 3.6.10(react@18.2.0) '@react-types/menu': 3.9.11(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14824,7 +14833,7 @@ packages: '@react-stately/form': 3.0.5(react@18.2.0) '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/numberfield': 3.8.5(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14835,7 +14844,7 @@ packages: dependencies: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/overlays': 3.8.9(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14872,7 +14881,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/radio': 3.8.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14894,7 +14903,7 @@ packages: dependencies: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/searchfield': 3.5.8(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14922,7 +14931,7 @@ packages: '@react-stately/overlays': 3.6.10(react@18.2.0) '@react-types/select': 3.9.6(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14946,7 +14955,7 @@ packages: '@react-stately/collections': 3.10.9(react@18.2.0) '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -14970,7 +14979,7 @@ packages: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/slider': 3.7.5(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15004,7 +15013,7 @@ packages: '@react-types/grid': 3.2.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/table': 3.10.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15028,7 +15037,7 @@ packages: '@react-stately/list': 3.10.8(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) '@react-types/tabs': 3.3.9(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15050,7 +15059,7 @@ packages: dependencies: '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/checkbox': 3.8.3(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15061,7 +15070,7 @@ packages: dependencies: '@react-stately/overlays': 3.6.10(react@18.2.0) '@react-types/tooltip': 3.4.11(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15098,7 +15107,7 @@ packages: '@react-stately/selection': 3.16.2(react@18.2.0) '@react-stately/utils': 3.10.3(react@18.2.0) '@react-types/shared': 3.24.1(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15107,7 +15116,7 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 dependencies: - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -15127,7 +15136,7 @@ packages: dependencies: '@react-aria/utils': 3.23.0(react@18.2.0) '@react-types/shared': 3.22.0(react@18.2.0) - '@swc/helpers': 0.5.5 + '@swc/helpers': 0.5.15 react: 18.2.0 dev: false @@ -17379,7 +17388,7 @@ packages: resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /@swc/types@0.1.6: @@ -19442,7 +19451,7 @@ packages: secure-json-parse: 2.7.0 svelte: 4.2.19 zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) transitivePeerDependencies: - solid-js - vue @@ -19468,7 +19477,7 @@ packages: jsondiffpatch: 0.6.0 react: 18.3.1 zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) dev: false /ai@4.0.2(react@18.3.1)(zod@3.23.8): @@ -19491,7 +19500,7 @@ packages: jsondiffpatch: 0.6.0 react: 18.3.1 zod: 3.23.8 - zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-json-schema: 3.24.3(zod@3.23.8) dev: true /ai@4.2.5(react@19.0.0)(zod@3.23.8): @@ -21007,6 +21016,7 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + requiresBuild: true /color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} @@ -22265,6 +22275,7 @@ packages: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 + dev: true /enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} @@ -22272,7 +22283,6 @@ packages: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 - dev: true /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -23926,7 +23936,7 @@ packages: resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} engines: {node: '>= 12'} dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: false /fill-range@7.0.1: @@ -24167,7 +24177,7 @@ packages: dependencies: react: 18.3.1 react-dom: 18.2.0(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false @@ -24283,7 +24293,7 @@ packages: resolution: {integrity: sha512-igqQZTtS4GFwkDWA5gFWQye9Lmkx184Y17+x9flFq8HC68RVuuQGPeQtBFdMMlnac7/2Bq1n+1rkp4S8ZAu7kA==} dependencies: adm-zip: 0.5.16 - tslib: 2.6.2 + tslib: 2.8.1 dev: false /generic-names@4.0.0: @@ -25242,6 +25252,7 @@ packages: /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + requiresBuild: true dev: false /is-bigint@1.0.4: @@ -27487,7 +27498,6 @@ packages: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: false /nanoid@3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} @@ -29235,7 +29245,7 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.7 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.0 dev: false @@ -29252,7 +29262,7 @@ packages: resolution: {integrity: sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.7 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.0 @@ -31513,6 +31523,7 @@ packages: /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + requiresBuild: true dependencies: is-arrayish: 0.3.2 dev: false @@ -32399,7 +32410,7 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/utils': 2.3.1 - tslib: 2.6.2 + tslib: 2.8.1 dev: true /tailwind-merge@1.12.0: @@ -34823,7 +34834,7 @@ packages: acorn-import-assertions: 1.9.0(acorn@8.12.1) browserslist: 4.23.3 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 + enhanced-resolve: 5.18.1 es-module-lexer: 1.3.1 eslint-scope: 5.1.1 events: 3.3.0 @@ -34863,7 +34874,7 @@ packages: acorn-import-assertions: 1.9.0(acorn@8.12.1) browserslist: 4.23.3 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 + enhanced-resolve: 5.18.1 es-module-lexer: 1.3.1 eslint-scope: 5.1.1 events: 3.3.0 @@ -35328,15 +35339,15 @@ packages: zod: 3.23.8 dev: false - /zod-to-json-schema@3.23.5(zod@3.23.8): - resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} + /zod-to-json-schema@3.24.3(zod@3.23.8): + resolution: {integrity: sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==} peerDependencies: - zod: ^3.23.3 + zod: ^3.24.1 dependencies: zod: 3.23.8 - /zod-to-json-schema@3.24.3(zod@3.23.8): - resolution: {integrity: sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==} + /zod-to-json-schema@3.24.5(zod@3.23.8): + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: zod: ^3.24.1 dependencies: diff --git a/references/agent-loop/src/trigger/schemas.ts b/references/agent-loop/src/trigger/schemas.ts deleted file mode 100644 index bc45de5d15..0000000000 --- a/references/agent-loop/src/trigger/schemas.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod"; - -export const AgentLoopMetadata = z.object({ - waitToken: z.object({ - id: z.string(), - publicAccessToken: z.string(), - }), -}); - -export type AgentLoopMetadata = z.infer; diff --git a/references/agent-loop/trigger.config.ts b/references/agent-loop/trigger.config.ts deleted file mode 100644 index 2e6be88c9f..0000000000 --- a/references/agent-loop/trigger.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "@trigger.dev/sdk"; - -export default defineConfig({ - project: "proj_hgknlxlflfcemqogujnt", - dirs: ["./src/trigger"], - maxDuration: 3600, -}); diff --git a/references/agent-loop/.gitignore b/references/d3-demo/.gitignore similarity index 100% rename from references/agent-loop/.gitignore rename to references/d3-demo/.gitignore diff --git a/references/agent-loop/README.md b/references/d3-demo/README.md similarity index 100% rename from references/agent-loop/README.md rename to references/d3-demo/README.md diff --git a/references/agent-loop/components.json b/references/d3-demo/components.json similarity index 100% rename from references/agent-loop/components.json rename to references/d3-demo/components.json diff --git a/references/agent-loop/next.config.ts b/references/d3-demo/next.config.ts similarity index 100% rename from references/agent-loop/next.config.ts rename to references/d3-demo/next.config.ts diff --git a/references/agent-loop/package.json b/references/d3-demo/package.json similarity index 56% rename from references/agent-loop/package.json rename to references/d3-demo/package.json index ade8dc4d25..da36771119 100644 --- a/references/agent-loop/package.json +++ b/references/d3-demo/package.json @@ -1,5 +1,5 @@ { - "name": "references-agent-loop", + "name": "references-d3-demo", "version": "0.1.0", "private": true, "scripts": { @@ -8,10 +8,18 @@ "start": "next start", "lint": "next lint", "dev:trigger": "trigger dev", - "deploy": "trigger deploy" + "deploy": "pnpm run python:compile-requirements && trigger deploy", + "python:install-requirements": "uv pip sync requirements.txt", + "python:compile-requirements": "uv pip compile requirements.in -o requirements.txt", + "python:install": "pnpm run python:compile-requirements && pnpm run python:install-requirements", + "python:create-env": "uv venv .venv" }, "dependencies": { "@ai-sdk/openai": "1.3.3", + "@trigger.dev/react-hooks": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "@trigger.dev/python": "workspace:*", + "ai": "4.2.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.484.0", @@ -20,10 +28,8 @@ "react-dom": "^19.0.0", "tailwind-merge": "^3.0.2", "tw-animate-css": "^1.2.4", - "@trigger.dev/react-hooks": "workspace:*", - "@trigger.dev/sdk": "workspace:*", - "ai": "4.2.5", - "zod": "3.23.8" + "zod": "3.23.8", + "zod-to-json-schema": "^3.24.5" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -31,7 +37,8 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4.0.17", - "typescript": "^5", - "trigger.dev": "workspace:*" + "trigger.dev": "workspace:*", + "@trigger.dev/build": "workspace:*", + "typescript": "^5" } } \ No newline at end of file diff --git a/references/agent-loop/postcss.config.mjs b/references/d3-demo/postcss.config.mjs similarity index 100% rename from references/agent-loop/postcss.config.mjs rename to references/d3-demo/postcss.config.mjs diff --git a/references/agent-loop/public/file.svg b/references/d3-demo/public/file.svg similarity index 100% rename from references/agent-loop/public/file.svg rename to references/d3-demo/public/file.svg diff --git a/references/agent-loop/public/globe.svg b/references/d3-demo/public/globe.svg similarity index 100% rename from references/agent-loop/public/globe.svg rename to references/d3-demo/public/globe.svg diff --git a/references/agent-loop/public/next.svg b/references/d3-demo/public/next.svg similarity index 100% rename from references/agent-loop/public/next.svg rename to references/d3-demo/public/next.svg diff --git a/references/agent-loop/public/vercel.svg b/references/d3-demo/public/vercel.svg similarity index 100% rename from references/agent-loop/public/vercel.svg rename to references/d3-demo/public/vercel.svg diff --git a/references/agent-loop/public/window.svg b/references/d3-demo/public/window.svg similarity index 100% rename from references/agent-loop/public/window.svg rename to references/d3-demo/public/window.svg diff --git a/references/d3-demo/requirements.in b/references/d3-demo/requirements.in new file mode 100644 index 0000000000..0616e8d8d7 --- /dev/null +++ b/references/d3-demo/requirements.in @@ -0,0 +1,5 @@ +crawl4ai +playwright +openai-agents +urllib3<2.0.0 +logfire \ No newline at end of file diff --git a/references/d3-demo/requirements.txt b/references/d3-demo/requirements.txt new file mode 100644 index 0000000000..5b1cfec6cc --- /dev/null +++ b/references/d3-demo/requirements.txt @@ -0,0 +1,309 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt +aiofiles==24.1.0 + # via crawl4ai +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.11.14 + # via + # crawl4ai + # litellm +aiosignal==1.3.2 + # via aiohttp +aiosqlite==0.21.0 + # via crawl4ai +annotated-types==0.7.0 + # via pydantic +anyio==4.9.0 + # via + # httpx + # mcp + # openai + # sse-starlette + # starlette +attrs==25.3.0 + # via + # aiohttp + # jsonschema + # referencing +beautifulsoup4==4.13.3 + # via crawl4ai +certifi==2025.1.31 + # via + # httpcore + # httpx + # requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.1 + # via requests +click==8.1.8 + # via + # crawl4ai + # litellm + # nltk + # uvicorn +colorama==0.4.6 + # via + # crawl4ai + # griffe +crawl4ai==0.5.0.post8 + # via -r requirements.in +cryptography==44.0.2 + # via pyopenssl +cssselect==1.3.0 + # via crawl4ai +deprecated==1.2.18 + # via + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-semantic-conventions +distro==1.9.0 + # via openai +executing==2.2.0 + # via logfire +fake-http-header==0.3.5 + # via tf-playwright-stealth +fake-useragent==2.1.0 + # via crawl4ai +faust-cchardet==2.1.19 + # via crawl4ai +filelock==3.18.0 + # via huggingface-hub +frozenlist==1.5.0 + # via + # aiohttp + # aiosignal +fsspec==2025.3.0 + # via huggingface-hub +googleapis-common-protos==1.69.2 + # via opentelemetry-exporter-otlp-proto-http +greenlet==3.1.1 + # via playwright +griffe==1.6.3 + # via openai-agents +h11==0.14.0 + # via + # httpcore + # uvicorn +httpcore==1.0.7 + # via httpx +httpx==0.28.1 + # via + # crawl4ai + # litellm + # mcp + # openai +httpx-sse==0.4.0 + # via mcp +huggingface-hub==0.29.3 + # via tokenizers +humanize==4.12.2 + # via crawl4ai +idna==3.10 + # via + # anyio + # httpx + # requests + # yarl +importlib-metadata==8.6.1 + # via + # litellm + # opentelemetry-api +jinja2==3.1.6 + # via litellm +jiter==0.9.0 + # via openai +joblib==1.4.2 + # via nltk +jsonschema==4.23.0 + # via litellm +jsonschema-specifications==2024.10.1 + # via jsonschema +litellm==1.64.1 + # via crawl4ai +logfire==3.11.0 + # via -r requirements.in +lxml==5.3.1 + # via crawl4ai +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via jinja2 +mcp==1.5.0 + # via openai-agents +mdurl==0.1.2 + # via markdown-it-py +multidict==6.2.0 + # via + # aiohttp + # yarl +nltk==3.9.1 + # via crawl4ai +numpy==2.2.4 + # via + # crawl4ai + # rank-bm25 +openai==1.68.2 + # via + # litellm + # openai-agents +openai-agents==0.0.7 + # via -r requirements.in +opentelemetry-api==1.31.1 + # via + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-instrumentation + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp-proto-common==1.31.1 + # via opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-http==1.31.1 + # via logfire +opentelemetry-instrumentation==0.52b1 + # via logfire +opentelemetry-proto==1.31.1 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.31.1 + # via + # logfire + # opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.52b1 + # via + # opentelemetry-instrumentation + # opentelemetry-sdk +packaging==24.2 + # via + # huggingface-hub + # opentelemetry-instrumentation +pillow==10.4.0 + # via crawl4ai +playwright==1.51.0 + # via + # -r requirements.in + # crawl4ai + # tf-playwright-stealth +propcache==0.3.1 + # via + # aiohttp + # yarl +protobuf==5.29.4 + # via + # googleapis-common-protos + # logfire + # opentelemetry-proto +psutil==7.0.0 + # via crawl4ai +pycparser==2.22 + # via cffi +pydantic==2.10.6 + # via + # crawl4ai + # litellm + # mcp + # openai + # openai-agents + # pydantic-settings +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.8.1 + # via mcp +pyee==12.1.1 + # via playwright +pygments==2.19.1 + # via rich +pyopenssl==25.0.0 + # via crawl4ai +pyperclip==1.9.0 + # via crawl4ai +python-dotenv==1.1.0 + # via + # crawl4ai + # litellm + # pydantic-settings +pyyaml==6.0.2 + # via huggingface-hub +rank-bm25==0.2.2 + # via crawl4ai +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +regex==2024.11.6 + # via + # nltk + # tiktoken +requests==2.32.3 + # via + # crawl4ai + # huggingface-hub + # openai-agents + # opentelemetry-exporter-otlp-proto-http + # tiktoken +rich==13.9.4 + # via + # crawl4ai + # logfire +rpds-py==0.24.0 + # via + # jsonschema + # referencing +sniffio==1.3.1 + # via + # anyio + # openai +snowballstemmer==2.2.0 + # via crawl4ai +soupsieve==2.6 + # via beautifulsoup4 +sse-starlette==2.2.1 + # via mcp +starlette==0.46.1 + # via + # mcp + # sse-starlette +tf-playwright-stealth==1.1.2 + # via crawl4ai +tiktoken==0.9.0 + # via litellm +tokenizers==0.21.1 + # via litellm +tqdm==4.67.1 + # via + # huggingface-hub + # nltk + # openai +types-requests==2.31.0.6 + # via openai-agents +types-urllib3==1.26.25.14 + # via types-requests +typing-extensions==4.13.0 + # via + # aiosqlite + # beautifulsoup4 + # huggingface-hub + # logfire + # openai + # openai-agents + # opentelemetry-sdk + # pydantic + # pydantic-core + # pyee +urllib3==1.26.20 + # via + # -r requirements.in + # requests +uvicorn==0.34.0 + # via mcp +wrapt==1.17.2 + # via + # deprecated + # opentelemetry-instrumentation +xxhash==3.5.0 + # via crawl4ai +yarl==1.18.3 + # via aiohttp +zipp==3.21.0 + # via importlib-metadata diff --git a/references/agent-loop/src/app/favicon.ico b/references/d3-demo/src/app/favicon.ico similarity index 100% rename from references/agent-loop/src/app/favicon.ico rename to references/d3-demo/src/app/favicon.ico diff --git a/references/agent-loop/src/app/globals.css b/references/d3-demo/src/app/globals.css similarity index 100% rename from references/agent-loop/src/app/globals.css rename to references/d3-demo/src/app/globals.css diff --git a/references/agent-loop/src/app/layout.tsx b/references/d3-demo/src/app/layout.tsx similarity index 100% rename from references/agent-loop/src/app/layout.tsx rename to references/d3-demo/src/app/layout.tsx diff --git a/references/agent-loop/src/app/page.tsx b/references/d3-demo/src/app/page.tsx similarity index 91% rename from references/agent-loop/src/app/page.tsx rename to references/d3-demo/src/app/page.tsx index 058c75c9f1..8bb6d2b9a0 100644 --- a/references/agent-loop/src/app/page.tsx +++ b/references/d3-demo/src/app/page.tsx @@ -2,7 +2,7 @@ import MainApp from "@/components/main-app"; import { auth } from "@trigger.dev/sdk"; export default async function Home() { - const publicAccessToken = await auth.createTriggerPublicToken("agent-loop-example"); + const publicAccessToken = await auth.createTriggerPublicToken("chat-example"); return ; } diff --git a/references/agent-loop/src/components/chat-interface.tsx b/references/d3-demo/src/components/chat-interface.tsx similarity index 100% rename from references/agent-loop/src/components/chat-interface.tsx rename to references/d3-demo/src/components/chat-interface.tsx diff --git a/references/agent-loop/src/components/initial-prompt.tsx b/references/d3-demo/src/components/initial-prompt.tsx similarity index 100% rename from references/agent-loop/src/components/initial-prompt.tsx rename to references/d3-demo/src/components/initial-prompt.tsx diff --git a/references/agent-loop/src/components/main-app.tsx b/references/d3-demo/src/components/main-app.tsx similarity index 88% rename from references/agent-loop/src/components/main-app.tsx rename to references/d3-demo/src/components/main-app.tsx index 210daf724d..fd10af527c 100644 --- a/references/agent-loop/src/components/main-app.tsx +++ b/references/d3-demo/src/components/main-app.tsx @@ -7,7 +7,7 @@ import { Send } from "lucide-react"; import ChatInterface from "@/components/chat-interface"; import InitialPrompt from "@/components/initial-prompt"; import { useRealtimeTaskTriggerWithStreams, useWaitToken } from "@trigger.dev/react-hooks"; -import type { agentLoopExample } from "@/trigger/agent"; +import type { chatExample } from "@/trigger/chat"; import { AgentLoopMetadata } from "@/trigger/schemas"; import type { TextStreamPart } from "ai"; @@ -17,14 +17,14 @@ type ResponseStreams = { [K in `responses.${number | string}`]: TextStreamPart<{}>; }; -export function useAgentLoop({ publicAccessToken }: { publicAccessToken: string }) { - const triggerInstance = useRealtimeTaskTriggerWithStreams< - typeof agentLoopExample, - ResponseStreams - >("agent-loop-example", { - accessToken: publicAccessToken, - baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, - }); +export function useChat({ publicAccessToken }: { publicAccessToken: string }) { + const triggerInstance = useRealtimeTaskTriggerWithStreams( + "chat-example", + { + accessToken: publicAccessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + } + ); const [conversation, setConversation] = useState([]); const [isLoading, setIsLoading] = useState(false); const [waitToken, setWaitToken] = useState(null); @@ -86,7 +86,7 @@ export function useAgentLoop({ publicAccessToken }: { publicAccessToken: string } export default function MainApp({ publicAccessToken }: { publicAccessToken: string }) { - const { continueConversation, conversation, isLoading } = useAgentLoop({ publicAccessToken }); + const { continueConversation, conversation, isLoading } = useChat({ publicAccessToken }); const [input, setInput] = useState(""); const handleSubmit = (e: React.FormEvent) => { diff --git a/references/d3-demo/src/extensions/playwright.ts b/references/d3-demo/src/extensions/playwright.ts new file mode 100644 index 0000000000..178bed23d1 --- /dev/null +++ b/references/d3-demo/src/extensions/playwright.ts @@ -0,0 +1,36 @@ +import type { BuildContext, BuildExtension } from "@trigger.dev/build"; + +// This is a custom build extension to install Playwright and Chromium +export function installPlaywrightChromium(): BuildExtension { + return { + name: "InstallPlaywrightChromium", + onBuildComplete(context: BuildContext) { + const instructions = [ + // Base and Chromium dependencies + `RUN apt-get update && apt-get install -y --no-install-recommends \ + curl unzip npm libnspr4 libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 \ + libasound2 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ + libgbm1 libxkbcommon0 \ + && apt-get clean && rm -rf /var/lib/apt/lists/*`, + + // Install Playwright and Chromium + `RUN npm install -g playwright`, + `RUN mkdir -p /ms-playwright`, + `RUN PLAYWRIGHT_BROWSERS_PATH=/ms-playwright python -m playwright install --with-deps chromium`, + ]; + + context.addLayer({ + id: "playwright", + image: { instructions }, + deploy: { + env: { + PLAYWRIGHT_BROWSERS_PATH: "/ms-playwright", + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1", + PLAYWRIGHT_SKIP_BROWSER_VALIDATION: "1", + }, + override: true, + }, + }); + }, + }; +} diff --git a/references/agent-loop/src/lib/utils.ts b/references/d3-demo/src/lib/utils.ts similarity index 100% rename from references/agent-loop/src/lib/utils.ts rename to references/d3-demo/src/lib/utils.ts diff --git a/references/agent-loop/src/trigger/agent.ts b/references/d3-demo/src/trigger/chat.ts similarity index 95% rename from references/agent-loop/src/trigger/agent.ts rename to references/d3-demo/src/trigger/chat.ts index ef59f5bee6..c2ca19821e 100644 --- a/references/agent-loop/src/trigger/agent.ts +++ b/references/d3-demo/src/trigger/chat.ts @@ -5,9 +5,9 @@ import { z } from "zod"; const MAX_STEPS = 10; -export const agentLoopExample = schemaTask({ - id: "agent-loop-example", - description: "Agent loop example", +export const chatExample = schemaTask({ + id: "chat-example", + description: "Chat example", schema: z.object({ model: z.string().default("chatgpt-4o-latest"), prompt: z.string().default("Hello, how are you?"), diff --git a/references/d3-demo/src/trigger/d3Demo.ts b/references/d3-demo/src/trigger/d3Demo.ts new file mode 100644 index 0000000000..148d8c8d07 --- /dev/null +++ b/references/d3-demo/src/trigger/d3Demo.ts @@ -0,0 +1,197 @@ +import { batch, logger, schemaTask, wait } from "@trigger.dev/sdk/v3"; +import { createHash } from "node:crypto"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { CSVRowPayload, RowEnrichmentResult } from "./schemas"; +import { python } from "@trigger.dev/python"; + +export const d3Demo = schemaTask({ + id: "d3-demo", + description: "D3 Demo - Enriches a CSV dataset using Trigger.dev and the python OpenAI Agent SDK", + schema: z.object({ + url: z.string().url().describe("The URL of the CSV dataset to enrich"), + }), + run: async ({ url }) => { + const response = await fetch(url); + const csv = await response.text(); + + const header = csv.split("\n")[0]; + const rows = csv.split("\n").slice(1); + + // Each payload has a header, url, and a single row + const payloads = rows.map((row) => ({ + header, + url, + row, + })); + + const chunkedPayloads = []; + + for (let i = 0; i < payloads.length; i += 500) { + chunkedPayloads.push(payloads.slice(i, i + 500)); + } + + // Process each chunk + for (const chunk of chunkedPayloads) { + const results = await batch.triggerAndWait( + chunk.map((payload) => ({ + id: "handle-csv-row", + payload, + options: { + idempotencyKey: createIdempotencyKey(payload), + }, + })) + ); + + for (const result of results.runs) { + if (result.ok) { + // Pretty print the result of the enrichment + logger.log(`Enriched row ${result.output.row}`, result.output); + } else { + logger.error(`Error enriching row: ${result.error}`); + } + } + } + }, +}); + +const ROW_AGENT_RUNNER_COUNT = 10; + +export const handleCSVRow = schemaTask({ + id: "handle-csv-row", + description: "Handles a single row of a CSV dataset", + schema: CSVRowPayload, + run: async ({ header, row, url }) => { + // Batch trigger the rowAgentCoordinator to try 10 different enrichments on the same row + const results = await batch.triggerAndWait( + Array.from({ length: ROW_AGENT_RUNNER_COUNT }, () => ({ + id: "row-agent-coordinator" as const, + payload: { row: { header, row, url } }, + })) + ); + + const outputs = results.runs.filter((result) => result.ok).map((result) => result.output); + + const enrichment = await enrichmentResultsEvaluator + .triggerAndWait({ + results: outputs, + }) + .unwrap(); + + return { + enrichment, + header, + row, + url, + }; + }, +}); + +export const rowAgentCoordinator = schemaTask({ + id: "row-agent-coordinator", + description: "Coordinators the row agent", + schema: z.object({ + row: CSVRowPayload, + }), + run: async ({ row }) => { + const waitToken = await wait.createToken({ + timeout: "2m", + }); + + const jsonSchema = zodToJsonSchema(RowEnrichmentResult); + + await rowAgentRunner.trigger({ + row, + waitToken, + jsonSchema, + }); + + const result = await wait.forToken(waitToken.id); + + if (!result.ok) { + throw result.error; + } + + return result.output; + }, +}); + +export const rowAgentRunner = schemaTask({ + id: "row-agent-runner", + description: "Runs the row agent", + schema: z.object({ + row: CSVRowPayload, + waitToken: z.object({ + id: z + .string() + .describe( + "The wait token that the OpenAI Agent SDK will use to signal back to the coordinator" + ), + publicAccessToken: z + .string() + .describe( + "The public access token that the OpenAI Agent SDK will use to signal back to the coordinator" + ), + }), + jsonSchema: z.any().describe("The JSON schema of the result"), + disableWaitTokenCompletion: z + .boolean() + .default(false) + .describe("Whether to disable wait token completion"), + }), + run: async ({ row, waitToken, jsonSchema, disableWaitTokenCompletion }) => { + const inputData = JSON.stringify({ + row, + waitToken, + jsonSchema, + disableWaitTokenCompletion, + }); + + logger.info("process.env", { + env: process.env, + }); + + const result = await python.runScript("./src/trigger/python/agent.py", [inputData]); + + logger.debug("row-agent-runner", { + result, + }); + + return {} as unknown as RowEnrichmentResult; + }, + catchError: async ({ error }) => { + logger.error("row-agent-runner", { + error, + }); + }, +}); + +export const enrichmentResultsEvaluator = schemaTask({ + id: "enrichment-results-evaluator", + description: "Evaluates the enrichment results", + schema: z.object({ + results: z.array(RowEnrichmentResult), + }), + run: async ({ results }) => { + // Combine the results into a single object (this is a placeholder for a more complex evaluation) + const combinedResult = results.reduce( + (acc, result) => ({ + ...acc, + ...result, + }), + {} as RowEnrichmentResult + ); + + return combinedResult; + }, +}); + +// Create a hash of the payload using Node.js crypto +// Ideally, you'd do a stable serialization of the payload before hashing, to ensure the same payload always results in the same hash +function createIdempotencyKey(payload: CSVRowPayload): string { + const content = `${payload.header}\n${payload.row}`; + + const hash = createHash("sha256"); + hash.update(content); + return hash.digest("hex"); +} diff --git a/references/d3-demo/src/trigger/python/agent.py b/references/d3-demo/src/trigger/python/agent.py new file mode 100644 index 0000000000..7b91296429 --- /dev/null +++ b/references/d3-demo/src/trigger/python/agent.py @@ -0,0 +1,171 @@ +import asyncio +import sys +import os +import json +import requests +from typing import Optional +from pydantic import BaseModel, Field +from agents import Agent, Runner, WebSearchTool, trace +import logfire +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from logfire.propagate import attach_context + +logfire.configure( + service_name='d3-demo', + send_to_logfire="if-token-present", + distributed_tracing=True +) +logfire.instrument_openai_agents() + +class CSVRowPayload(BaseModel): + header: str = Field(description="The header of the CSV dataset") + row: str = Field(description="The row to handle") + url: str = Field(description="The URL of the CSV dataset") + +class WaitToken(BaseModel): + id: str = Field(description="The wait token ID") + publicAccessToken: str = Field(description="The public access token for authorization") + +class AgentInput(BaseModel): + row: CSVRowPayload + waitToken: WaitToken + jsonSchema: dict + disableWaitTokenCompletion: bool = Field(default=False, description="Whether to disable wait token completion") + +class BasicInfo(BaseModel): + name: str = Field(description="The name of the row") + email: str = Field(description="The email of the row") + firstName: Optional[str] = Field(None, description="The first name of the row") + lastName: Optional[str] = Field(None, description="The last name of the row") + preferredName: Optional[str] = Field(None, description="The preferred name of the row") + +class CompanyInfo(BaseModel): + name: str = Field(description="The name of the company") + industry: str = Field(description="The industry of the company") + +class SocialInfo(BaseModel): + twitter: Optional[str] = Field(None, description="The Twitter username of the person") + linkedin: Optional[str] = Field(None, description="The LinkedIn username of the person") + facebook: Optional[str] = Field(None, description="The Facebook username of the person") + instagram: Optional[str] = Field(None, description="The Instagram username of the person") + +class RowEnrichmentResult(BaseModel): + basicInfo: BasicInfo + companyInfo: CompanyInfo + socialInfo: SocialInfo + +def complete_wait_token(wait_token: WaitToken, result: dict): + """Send the enrichment result back to Trigger.dev""" + with trace("complete_wait_token"): + url = f"{os.environ['TRIGGER_API_URL']}/api/v1/waitpoints/tokens/{wait_token.id}/complete" + headers = { + "Authorization": f"Bearer {wait_token.publicAccessToken}", + "Content-Type": "application/json" + } + response = requests.post(url, json={"data": result}, headers=headers) + response.raise_for_status() + return response.json() + +basic_info_agent = Agent( + name="Basic Info Agent", + instructions=f"""You are an expert at extracting basic information from a person's contact details. + """, + tools=[WebSearchTool()], + output_type=BasicInfo +) + +company_info_agent = Agent( + name="Company Info Agent", + instructions=f"""You are an expert at extracting company information from a person's contact details. + """, + tools=[WebSearchTool()], + output_type=CompanyInfo +) + +social_info_agent = Agent( + name="Social Info Agent", + instructions=f"""You are an expert at extracting social media information from a person's contact details. + """, + tools=[WebSearchTool()], + output_type=SocialInfo +) + +agent = Agent( + name="CSV Row Enrichment Agent", + instructions=f"""You are an expert at enriching contact information data. + Your task is to use web search to find additional information about a person + based on their basic contact details. + + Please find the basic information, company information, and social media information for the person. + + The input data is from an unspecified CSV dataset, but most likely a CSV dump from a CRM or other source like Mailchimp, etc. + + You'll receive the header row and a single row from the dataset to enrich, in their raw form. + + Only include information you are confident about from reliable sources. + Use null for fields where you cannot find reliable information. + """, + tools=[ + basic_info_agent.as_tool( + tool_name="basic_info_agent", + tool_description="Extract basic information from a person's contact details" + ), + company_info_agent.as_tool( + tool_name="company_info_agent", + tool_description="Extract company information from a person's contact details" + ), + social_info_agent.as_tool( + tool_name="social_info_agent", + tool_description="Extract social media information from a person's contact details" + ) + ], + output_type=RowEnrichmentResult +) + +async def main(agent_input: AgentInput): + # Run the agent + result = await Runner.run( + agent, + f""" + Header row: {agent_input.row.header} + Row to enrich: {agent_input.row.row} + CSV URL: {agent_input.row.url} + """ + ) + + enriched_data = result.final_output + if isinstance(enriched_data, BaseModel): + enriched_data = enriched_data.model_dump() + + print("Final Output:") + # Pretty print the final output + print(json.dumps(enriched_data, indent=2)) + + if not agent_input.disableWaitTokenCompletion: + try: + # Send the result back to Trigger.dev + complete_wait_token(agent_input.waitToken, enriched_data) + + print("Successfully enriched data and notified Trigger.dev") + + except json.JSONDecodeError: + print("Error: Agent output was not valid JSON") + sys.exit(1) + + # Make sure to flush the logfire context + logfire.force_flush() + +if __name__ == "__main__": + # Parse command line input as JSON + if len(sys.argv) < 2: + print("Usage: python agent.py ''") + sys.exit(1) + + # Extract the traceparent os.environ['TRACEPARENT'] + carrier ={'traceparent': os.environ['TRACEPARENT']} + ctx = TraceContextTextMapPropagator().extract(carrier=carrier) + + with attach_context(carrier=carrier, third_party=True): + # Parse the input JSON into our Pydantic model + input_data = AgentInput.model_validate_json(sys.argv[1]) + asyncio.run(main(input_data)) \ No newline at end of file diff --git a/references/d3-demo/src/trigger/schemas.ts b/references/d3-demo/src/trigger/schemas.ts new file mode 100644 index 0000000000..5c51a1a39b --- /dev/null +++ b/references/d3-demo/src/trigger/schemas.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const AgentLoopMetadata = z.object({ + waitToken: z.object({ + id: z.string(), + publicAccessToken: z.string(), + }), +}); + +export type AgentLoopMetadata = z.infer; + +export const CSVRowPayload = z.object({ + header: z.string().describe("The header of the CSV dataset"), + row: z.string().describe("The row to handle"), + url: z.string().url().describe("The URL of the CSV dataset"), +}); + +export type CSVRowPayload = z.infer; + +export const RowEnrichmentResult = z.object({ + basicInfo: z.object({ + name: z.string().describe("The name of the row"), + email: z.string().email().describe("The email of the row"), + firstName: z.string().optional().describe("The first name of the row"), + lastName: z.string().optional().describe("The last name of the row"), + preferredName: z.string().optional().describe("The preferred name of the row"), + }), + companyInfo: z.object({ + name: z.string().describe("The name of the company"), + industry: z.string().describe("The industry of the company"), + }), + socialInfo: z.object({ + twitter: z.string().url().optional().describe("The Twitter URL of the person"), + linkedin: z.string().url().optional().describe("The LinkedIn URL of the person"), + facebook: z.string().url().optional().describe("The Facebook URL of the person"), + instagram: z.string().url().optional().describe("The Instagram URL of the person"), + }), +}); + +export type RowEnrichmentResult = z.infer; diff --git a/references/agent-loop/tailwind.config.ts b/references/d3-demo/tailwind.config.ts similarity index 100% rename from references/agent-loop/tailwind.config.ts rename to references/d3-demo/tailwind.config.ts diff --git a/references/d3-demo/trigger.config.ts b/references/d3-demo/trigger.config.ts new file mode 100644 index 0000000000..77bce09ee8 --- /dev/null +++ b/references/d3-demo/trigger.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "@trigger.dev/sdk"; +import { pythonExtension } from "@trigger.dev/python/extension"; +import { installPlaywrightChromium } from "./src/extensions/playwright"; + +export default defineConfig({ + project: "proj_wkbbtayxrmeyqhankehb", + dirs: ["./src/trigger"], + maxDuration: 3600, + build: { + extensions: [ + // This is required to use the Python extension + pythonExtension({ + requirementsFile: "./requirements.txt", // Optional: Path to your requirements file + devPythonBinaryPath: `.venv/bin/python`, // Optional: Custom Python binary path + scripts: ["src/trigger/python/**/*.py"], // List of Python scripts to include + }), + installPlaywrightChromium(), + ], + }, +}); diff --git a/references/agent-loop/tsconfig.json b/references/d3-demo/tsconfig.json similarity index 100% rename from references/agent-loop/tsconfig.json rename to references/d3-demo/tsconfig.json