@looopy-ai/core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/dist/core/agent.d.ts +53 -0
- package/dist/core/agent.js +416 -0
- package/dist/core/cleanup.d.ts +12 -0
- package/dist/core/cleanup.js +45 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/iteration.d.ts +5 -0
- package/dist/core/iteration.js +60 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +31 -0
- package/dist/core/loop.d.ts +5 -0
- package/dist/core/loop.js +125 -0
- package/dist/core/tools.d.ts +4 -0
- package/dist/core/tools.js +78 -0
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.js +1 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +2 -0
- package/dist/events/utils.d.ts +250 -0
- package/dist/events/utils.js +263 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/spans/agent-turn.d.ts +31 -0
- package/dist/observability/spans/agent-turn.js +94 -0
- package/dist/observability/spans/index.d.ts +5 -0
- package/dist/observability/spans/index.js +5 -0
- package/dist/observability/spans/iteration.d.ts +14 -0
- package/dist/observability/spans/iteration.js +41 -0
- package/dist/observability/spans/llm-call.d.ts +14 -0
- package/dist/observability/spans/llm-call.js +50 -0
- package/dist/observability/spans/loop.d.ts +14 -0
- package/dist/observability/spans/loop.js +40 -0
- package/dist/observability/spans/tool.d.ts +14 -0
- package/dist/observability/spans/tool.js +44 -0
- package/dist/observability/tracing.d.ts +58 -0
- package/dist/observability/tracing.js +203 -0
- package/dist/providers/chat-completions/aggregate.d.ts +4 -0
- package/dist/providers/chat-completions/aggregate.js +152 -0
- package/dist/providers/chat-completions/content.d.ts +25 -0
- package/dist/providers/chat-completions/content.js +229 -0
- package/dist/providers/chat-completions/index.d.ts +4 -0
- package/dist/providers/chat-completions/index.js +4 -0
- package/dist/providers/chat-completions/streaming.d.ts +12 -0
- package/dist/providers/chat-completions/streaming.js +3 -0
- package/dist/providers/chat-completions/types.d.ts +39 -0
- package/dist/providers/chat-completions/types.js +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/litellm-provider.d.ts +43 -0
- package/dist/providers/litellm-provider.js +377 -0
- package/dist/server/event-buffer.d.ts +37 -0
- package/dist/server/event-buffer.js +116 -0
- package/dist/server/event-router.d.ts +31 -0
- package/dist/server/event-router.js +91 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/server/sse.d.ts +60 -0
- package/dist/server/sse.js +159 -0
- package/dist/stores/artifacts/artifact-scheduler.d.ts +50 -0
- package/dist/stores/artifacts/artifact-scheduler.js +86 -0
- package/dist/stores/artifacts/index.d.ts +3 -0
- package/dist/stores/artifacts/index.js +3 -0
- package/dist/stores/artifacts/internal-event-artifact-store.d.ts +54 -0
- package/dist/stores/artifacts/internal-event-artifact-store.js +126 -0
- package/dist/stores/artifacts/memory-artifact-store.d.ts +52 -0
- package/dist/stores/artifacts/memory-artifact-store.js +268 -0
- package/dist/stores/filesystem/filesystem-agent-store.d.ts +18 -0
- package/dist/stores/filesystem/filesystem-agent-store.js +61 -0
- package/dist/stores/filesystem/filesystem-artifact-store.d.ts +59 -0
- package/dist/stores/filesystem/filesystem-artifact-store.js +325 -0
- package/dist/stores/filesystem/filesystem-context-store.d.ts +37 -0
- package/dist/stores/filesystem/filesystem-context-store.js +245 -0
- package/dist/stores/filesystem/filesystem-message-store.d.ts +28 -0
- package/dist/stores/filesystem/filesystem-message-store.js +149 -0
- package/dist/stores/filesystem/filesystem-task-state-store.d.ts +27 -0
- package/dist/stores/filesystem/filesystem-task-state-store.js +220 -0
- package/dist/stores/filesystem/index.d.ts +10 -0
- package/dist/stores/filesystem/index.js +5 -0
- package/dist/stores/index.d.ts +5 -0
- package/dist/stores/index.js +5 -0
- package/dist/stores/memory/memory-state-store.d.ts +15 -0
- package/dist/stores/memory/memory-state-store.js +55 -0
- package/dist/stores/messages/hybrid-message-store.d.ts +29 -0
- package/dist/stores/messages/hybrid-message-store.js +72 -0
- package/dist/stores/messages/index.d.ts +4 -0
- package/dist/stores/messages/index.js +4 -0
- package/dist/stores/messages/interfaces.d.ts +42 -0
- package/dist/stores/messages/interfaces.js +18 -0
- package/dist/stores/messages/mem0-message-store.d.ts +34 -0
- package/dist/stores/messages/mem0-message-store.js +218 -0
- package/dist/stores/messages/memory-message-store.d.ts +27 -0
- package/dist/stores/messages/memory-message-store.js +183 -0
- package/dist/tools/artifact-tools.d.ts +4 -0
- package/dist/tools/artifact-tools.js +277 -0
- package/dist/tools/client-tool-provider.d.ts +25 -0
- package/dist/tools/client-tool-provider.js +139 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/local-tools.d.ts +13 -0
- package/dist/tools/local-tools.js +70 -0
- package/dist/tools/mcp-client.d.ts +29 -0
- package/dist/tools/mcp-client.js +62 -0
- package/dist/tools/mcp-tool-provider.d.ts +22 -0
- package/dist/tools/mcp-tool-provider.js +86 -0
- package/dist/types/a2a.d.ts +36 -0
- package/dist/types/a2a.js +1 -0
- package/dist/types/agent.d.ts +14 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/artifact.d.ts +126 -0
- package/dist/types/artifact.js +1 -0
- package/dist/types/context.d.ts +13 -0
- package/dist/types/context.js +1 -0
- package/dist/types/event.d.ts +360 -0
- package/dist/types/event.js +30 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +9 -0
- package/dist/types/llm.d.ts +24 -0
- package/dist/types/llm.js +1 -0
- package/dist/types/message.d.ts +9 -0
- package/dist/types/message.js +1 -0
- package/dist/types/state.d.ts +86 -0
- package/dist/types/state.js +1 -0
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/tools.js +53 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +23 -0
- package/dist/utils/process-signals.d.ts +3 -0
- package/dist/utils/process-signals.js +67 -0
- package/package.json +54 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Tracer } from '@opentelemetry/api';
|
|
2
|
+
export interface TraceContext {
|
|
3
|
+
traceId: string;
|
|
4
|
+
spanId: string;
|
|
5
|
+
traceFlags?: number;
|
|
6
|
+
traceState?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TelemetryConfig {
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
serviceVersion?: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
otlpEndpoint?: string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function initializeTracing(config?: TelemetryConfig): Tracer;
|
|
16
|
+
export declare function getTracer(): Tracer;
|
|
17
|
+
export declare function shutdownTracing(): Promise<void>;
|
|
18
|
+
export declare const SpanAttributes: {
|
|
19
|
+
readonly SESSION_ID: "session.id";
|
|
20
|
+
readonly USER_ID: "user.id";
|
|
21
|
+
readonly AGENT_ID: "agent.id";
|
|
22
|
+
readonly TASK_ID: "agent.task.id";
|
|
23
|
+
readonly ITERATION: "agent.iteration";
|
|
24
|
+
readonly TOOL_NAME: "agent.tool.name";
|
|
25
|
+
readonly TOOL_CALL_ID: "agent.tool.call_id";
|
|
26
|
+
readonly LLM_MODEL: "llm.model";
|
|
27
|
+
readonly LLM_FINISH_REASON: "llm.finish_reason";
|
|
28
|
+
readonly SUB_AGENT_ID: "agent.sub_agent.id";
|
|
29
|
+
readonly SUB_AGENT_TASK_ID: "agent.sub_agent.task_id";
|
|
30
|
+
readonly OUTPUT: "output";
|
|
31
|
+
readonly LANGFUSE_OBSERVATION_TYPE: "langfuse.observation.type";
|
|
32
|
+
readonly LANGFUSE_TAGS: "langfuse.tags";
|
|
33
|
+
readonly LANGFUSE_METADATA: "langfuse.metadata";
|
|
34
|
+
readonly LANGFUSE_VERSION: "langfuse.version";
|
|
35
|
+
readonly LANGFUSE_RELEASE: "langfuse.release";
|
|
36
|
+
readonly GEN_AI_SYSTEM: "gen_ai.system";
|
|
37
|
+
readonly GEN_AI_REQUEST_MODEL: "gen_ai.request.model";
|
|
38
|
+
readonly GEN_AI_RESPONSE_MODEL: "gen_ai.response.model";
|
|
39
|
+
readonly GEN_AI_PROMPT: "gen_ai.prompt";
|
|
40
|
+
readonly GEN_AI_COMPLETION: "gen_ai.completion";
|
|
41
|
+
readonly GEN_AI_USAGE_PROMPT_TOKENS: "gen_ai.usage.prompt_tokens";
|
|
42
|
+
readonly GEN_AI_USAGE_COMPLETION_TOKENS: "gen_ai.usage.completion_tokens";
|
|
43
|
+
readonly GEN_AI_USAGE_TOTAL_TOKENS: "gen_ai.usage.total_tokens";
|
|
44
|
+
readonly GEN_AI_USAGE_COMPLETION_TOKENS_DETAILS: "gen_ai.usage.completion_tokens_details";
|
|
45
|
+
readonly GEN_AI_USAGE_PROMPT_TOKENS_DETAILS: "gen_ai.usage.prompt_tokens_details";
|
|
46
|
+
readonly GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: "gen_ai.usage.cache_creation_input_tokens";
|
|
47
|
+
readonly GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: "gen_ai.usage.cache_read_input_tokens";
|
|
48
|
+
};
|
|
49
|
+
export declare const SpanNames: {
|
|
50
|
+
readonly LOOP_START: "loop.start";
|
|
51
|
+
readonly LOOP_ITERATION: "loop.iteration";
|
|
52
|
+
readonly LLM_CALL: "llm.call";
|
|
53
|
+
readonly TOOL_EXECUTE: "tool.execute";
|
|
54
|
+
readonly TOOL_PROVIDE: "tool.provide";
|
|
55
|
+
readonly SUB_AGENT_INVOKE: "agent.invoke";
|
|
56
|
+
readonly CHECKPOINT_SAVE: "checkpoint.save";
|
|
57
|
+
readonly CHECKPOINT_LOAD: "checkpoint.load";
|
|
58
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { trace } from '@opentelemetry/api';
|
|
2
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
3
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
4
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
6
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions/incubating';
|
|
7
|
+
import { DEFAULT_LOGGER } from '../core/logger';
|
|
8
|
+
let tracerProvider = null;
|
|
9
|
+
let defaultTracer = null;
|
|
10
|
+
function parseOTLPError(error) {
|
|
11
|
+
const errorDetails = {
|
|
12
|
+
message: error instanceof Error ? error.message : String(error),
|
|
13
|
+
};
|
|
14
|
+
if (!error || typeof error !== 'object') {
|
|
15
|
+
return errorDetails;
|
|
16
|
+
}
|
|
17
|
+
if ('code' in error) {
|
|
18
|
+
errorDetails.statusCode = error.code;
|
|
19
|
+
}
|
|
20
|
+
if ('data' in error && typeof error.data === 'string') {
|
|
21
|
+
const data = error.data;
|
|
22
|
+
errorDetails.responsePreview =
|
|
23
|
+
data.length > 500
|
|
24
|
+
? `${data.substring(0, 500)}... (truncated ${data.length - 500} chars)`
|
|
25
|
+
: data;
|
|
26
|
+
if (data.includes('<!DOCTYPE html>')) {
|
|
27
|
+
errorDetails.likelyIssue =
|
|
28
|
+
'Endpoint returned HTML instead of accepting OTLP - wrong URL or missing/invalid auth';
|
|
29
|
+
}
|
|
30
|
+
else if (data.includes('Unauthorized') || data.includes('401')) {
|
|
31
|
+
errorDetails.likelyIssue = 'Authentication failed - check OTEL_EXPORTER_OTLP_HEADERS';
|
|
32
|
+
}
|
|
33
|
+
else if (data.includes('Forbidden') || data.includes('403')) {
|
|
34
|
+
errorDetails.likelyIssue = 'Access forbidden - check API keys have correct permissions';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if ('message' in error) {
|
|
38
|
+
errorDetails.errorMessage = error.message;
|
|
39
|
+
}
|
|
40
|
+
return errorDetails;
|
|
41
|
+
}
|
|
42
|
+
function validateAuthHeaders(endpoint) {
|
|
43
|
+
const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
|
|
44
|
+
if (!headers)
|
|
45
|
+
return;
|
|
46
|
+
const hasBasicAuth = headers.includes('Authorization=Basic');
|
|
47
|
+
const hasCustomHeaders = !hasBasicAuth && headers.includes('=');
|
|
48
|
+
DEFAULT_LOGGER.trace({
|
|
49
|
+
authType: hasBasicAuth ? 'Basic Auth' : hasCustomHeaders ? 'Custom Headers' : 'Unknown',
|
|
50
|
+
headerCount: headers.split(',').length,
|
|
51
|
+
}, 'Authentication headers configured');
|
|
52
|
+
if (endpoint.includes('langfuse.com') && !hasBasicAuth) {
|
|
53
|
+
DEFAULT_LOGGER.warn('Langfuse requires Basic Auth format: Authorization=Basic <base64>. Current headers may not work.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function validateOTLPEndpoint(endpoint) {
|
|
57
|
+
if (endpoint.includes('langfuse.com')) {
|
|
58
|
+
if (!endpoint.endsWith('/v1/traces')) {
|
|
59
|
+
DEFAULT_LOGGER.trace({
|
|
60
|
+
endpoint,
|
|
61
|
+
correctEndpoint: endpoint.replace(/\/?$/, '/v1/traces'),
|
|
62
|
+
}, 'Langfuse OTLP endpoint should end with /v1/traces. Exports may fail without this.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (endpoint.includes('/api/public/otel') && !endpoint.includes('/v1/traces')) {
|
|
66
|
+
DEFAULT_LOGGER.trace({
|
|
67
|
+
endpoint,
|
|
68
|
+
suggestion: `${endpoint}/v1/traces`,
|
|
69
|
+
}, 'OTLP endpoint appears incomplete. Most OTLP collectors require /v1/traces path.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function initializeTracing(config) {
|
|
73
|
+
if (!config?.enabled && process.env.OTEL_ENABLED !== 'true') {
|
|
74
|
+
DEFAULT_LOGGER.trace('OpenTelemetry tracing is disabled');
|
|
75
|
+
return trace.getTracer('looopy-noop');
|
|
76
|
+
}
|
|
77
|
+
const serviceName = config?.serviceName || 'looopy';
|
|
78
|
+
const serviceVersion = config?.serviceVersion || '1.0.0';
|
|
79
|
+
const environment = config?.environment || process.env.NODE_ENV || 'development';
|
|
80
|
+
DEFAULT_LOGGER.trace({
|
|
81
|
+
serviceName,
|
|
82
|
+
serviceVersion,
|
|
83
|
+
environment,
|
|
84
|
+
}, 'Initializing OpenTelemetry tracing');
|
|
85
|
+
const resource = resourceFromAttributes({
|
|
86
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
87
|
+
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
88
|
+
'deployment.environment': environment,
|
|
89
|
+
});
|
|
90
|
+
const otlpEndpoint = config?.otlpEndpoint ||
|
|
91
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
|
|
92
|
+
'http://localhost:4318/v1/traces';
|
|
93
|
+
const hasAuthHeaders = !!process.env.OTEL_EXPORTER_OTLP_HEADERS;
|
|
94
|
+
DEFAULT_LOGGER.trace({
|
|
95
|
+
otlpEndpoint,
|
|
96
|
+
hasAuthHeaders,
|
|
97
|
+
}, 'Configuring OTLP exporter');
|
|
98
|
+
validateOTLPEndpoint(otlpEndpoint);
|
|
99
|
+
if (hasAuthHeaders) {
|
|
100
|
+
validateAuthHeaders(otlpEndpoint);
|
|
101
|
+
}
|
|
102
|
+
else if (otlpEndpoint.includes('cloud.langfuse.com')) {
|
|
103
|
+
DEFAULT_LOGGER.trace({
|
|
104
|
+
otlpEndpoint,
|
|
105
|
+
}, 'Using Langfuse cloud endpoint without OTEL_EXPORTER_OTLP_HEADERS. Authentication will likely fail.');
|
|
106
|
+
}
|
|
107
|
+
const exporter = new OTLPTraceExporter({
|
|
108
|
+
url: otlpEndpoint,
|
|
109
|
+
});
|
|
110
|
+
const originalExport = exporter.export.bind(exporter);
|
|
111
|
+
exporter.export = (spans, resultCallback) => {
|
|
112
|
+
DEFAULT_LOGGER.trace({
|
|
113
|
+
spanCount: spans.length,
|
|
114
|
+
endpoint: otlpEndpoint,
|
|
115
|
+
}, 'Exporting spans to OTLP collector');
|
|
116
|
+
originalExport(spans, (result) => {
|
|
117
|
+
if (result.code !== 0) {
|
|
118
|
+
DEFAULT_LOGGER.trace(parseOTLPError(result.error), 'Failed to export spans to OTLP collector');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
DEFAULT_LOGGER.trace({ spanCount: spans.length }, 'Successfully exported spans');
|
|
122
|
+
}
|
|
123
|
+
resultCallback(result);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
const spanProcessor = new BatchSpanProcessor(exporter, {
|
|
127
|
+
maxQueueSize: 1000,
|
|
128
|
+
scheduledDelayMillis: 1000,
|
|
129
|
+
maxExportBatchSize: 512,
|
|
130
|
+
});
|
|
131
|
+
tracerProvider = new NodeTracerProvider({
|
|
132
|
+
resource,
|
|
133
|
+
spanProcessors: [spanProcessor],
|
|
134
|
+
});
|
|
135
|
+
tracerProvider.register();
|
|
136
|
+
DEFAULT_LOGGER.trace('OpenTelemetry tracing initialized successfully');
|
|
137
|
+
defaultTracer = tracerProvider.getTracer(serviceName, serviceVersion);
|
|
138
|
+
return defaultTracer;
|
|
139
|
+
}
|
|
140
|
+
export function getTracer() {
|
|
141
|
+
if (!defaultTracer) {
|
|
142
|
+
return initializeTracing();
|
|
143
|
+
}
|
|
144
|
+
return defaultTracer;
|
|
145
|
+
}
|
|
146
|
+
export async function shutdownTracing() {
|
|
147
|
+
if (tracerProvider) {
|
|
148
|
+
DEFAULT_LOGGER.trace('Shutting down OpenTelemetry tracing');
|
|
149
|
+
try {
|
|
150
|
+
await tracerProvider.shutdown();
|
|
151
|
+
DEFAULT_LOGGER.trace('OpenTelemetry tracing shutdown complete');
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const errorDetails = parseOTLPError(error);
|
|
155
|
+
DEFAULT_LOGGER.error(errorDetails, 'Error during OpenTelemetry shutdown (collector may be unavailable or misconfigured)');
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
tracerProvider = null;
|
|
159
|
+
defaultTracer = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export const SpanAttributes = {
|
|
164
|
+
SESSION_ID: 'session.id',
|
|
165
|
+
USER_ID: 'user.id',
|
|
166
|
+
AGENT_ID: 'agent.id',
|
|
167
|
+
TASK_ID: 'agent.task.id',
|
|
168
|
+
ITERATION: 'agent.iteration',
|
|
169
|
+
TOOL_NAME: 'agent.tool.name',
|
|
170
|
+
TOOL_CALL_ID: 'agent.tool.call_id',
|
|
171
|
+
LLM_MODEL: 'llm.model',
|
|
172
|
+
LLM_FINISH_REASON: 'llm.finish_reason',
|
|
173
|
+
SUB_AGENT_ID: 'agent.sub_agent.id',
|
|
174
|
+
SUB_AGENT_TASK_ID: 'agent.sub_agent.task_id',
|
|
175
|
+
OUTPUT: 'output',
|
|
176
|
+
LANGFUSE_OBSERVATION_TYPE: 'langfuse.observation.type',
|
|
177
|
+
LANGFUSE_TAGS: 'langfuse.tags',
|
|
178
|
+
LANGFUSE_METADATA: 'langfuse.metadata',
|
|
179
|
+
LANGFUSE_VERSION: 'langfuse.version',
|
|
180
|
+
LANGFUSE_RELEASE: 'langfuse.release',
|
|
181
|
+
GEN_AI_SYSTEM: 'gen_ai.system',
|
|
182
|
+
GEN_AI_REQUEST_MODEL: 'gen_ai.request.model',
|
|
183
|
+
GEN_AI_RESPONSE_MODEL: 'gen_ai.response.model',
|
|
184
|
+
GEN_AI_PROMPT: 'gen_ai.prompt',
|
|
185
|
+
GEN_AI_COMPLETION: 'gen_ai.completion',
|
|
186
|
+
GEN_AI_USAGE_PROMPT_TOKENS: 'gen_ai.usage.prompt_tokens',
|
|
187
|
+
GEN_AI_USAGE_COMPLETION_TOKENS: 'gen_ai.usage.completion_tokens',
|
|
188
|
+
GEN_AI_USAGE_TOTAL_TOKENS: 'gen_ai.usage.total_tokens',
|
|
189
|
+
GEN_AI_USAGE_COMPLETION_TOKENS_DETAILS: 'gen_ai.usage.completion_tokens_details',
|
|
190
|
+
GEN_AI_USAGE_PROMPT_TOKENS_DETAILS: 'gen_ai.usage.prompt_tokens_details',
|
|
191
|
+
GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: 'gen_ai.usage.cache_creation_input_tokens',
|
|
192
|
+
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: 'gen_ai.usage.cache_read_input_tokens',
|
|
193
|
+
};
|
|
194
|
+
export const SpanNames = {
|
|
195
|
+
LOOP_START: 'loop.start',
|
|
196
|
+
LOOP_ITERATION: 'loop.iteration',
|
|
197
|
+
LLM_CALL: 'llm.call',
|
|
198
|
+
TOOL_EXECUTE: 'tool.execute',
|
|
199
|
+
TOOL_PROVIDE: 'tool.provide',
|
|
200
|
+
SUB_AGENT_INVOKE: 'agent.invoke',
|
|
201
|
+
CHECKPOINT_SAVE: 'checkpoint.save',
|
|
202
|
+
CHECKPOINT_LOAD: 'checkpoint.load',
|
|
203
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type OperatorFunction } from 'rxjs';
|
|
2
|
+
import type { Choice, LLMUsage } from './types';
|
|
3
|
+
export declare const aggregateChoice: <T extends Choice>() => OperatorFunction<T, T>;
|
|
4
|
+
export declare const aggregateLLMUsage: <T extends LLMUsage>() => OperatorFunction<T, T>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { InlineXmlParser } from './content';
|
|
3
|
+
const mergeToolCallDelta = (existing, delta) => {
|
|
4
|
+
if (delta.id) {
|
|
5
|
+
existing.id = delta.id;
|
|
6
|
+
}
|
|
7
|
+
if (delta.function?.name) {
|
|
8
|
+
existing.function.name = delta.function.name;
|
|
9
|
+
}
|
|
10
|
+
if (delta.function?.arguments) {
|
|
11
|
+
existing.function.arguments += delta.function.arguments;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const createToolCallAccumulator = (delta) => ({
|
|
15
|
+
id: delta.id ?? null,
|
|
16
|
+
type: 'function',
|
|
17
|
+
function: {
|
|
18
|
+
name: delta.function?.name || '',
|
|
19
|
+
arguments: delta.function?.arguments || '',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const processToolCallDeltas = (toolCallsByIndex, deltas) => {
|
|
23
|
+
for (const toolCallDelta of deltas) {
|
|
24
|
+
const idx = toolCallDelta.index;
|
|
25
|
+
const existing = toolCallsByIndex.get(idx);
|
|
26
|
+
if (!existing) {
|
|
27
|
+
toolCallsByIndex.set(idx, createToolCallAccumulator(toolCallDelta));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
mergeToolCallDelta(existing, toolCallDelta);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const finalizeToolCalls = (toolCallsByIndex) => {
|
|
35
|
+
return Array.from(toolCallsByIndex.entries())
|
|
36
|
+
.sort(([a], [b]) => a - b)
|
|
37
|
+
.map(([idx, tc]) => ({
|
|
38
|
+
index: idx,
|
|
39
|
+
id: tc.id,
|
|
40
|
+
type: tc.type,
|
|
41
|
+
function: tc.function,
|
|
42
|
+
}));
|
|
43
|
+
};
|
|
44
|
+
const processChoice = (choice, aggregated, toolCallsByIndex, xmlParser) => {
|
|
45
|
+
if (aggregated.index === undefined) {
|
|
46
|
+
aggregated.index = choice.index;
|
|
47
|
+
}
|
|
48
|
+
if (choice.delta) {
|
|
49
|
+
if (choice.delta.content) {
|
|
50
|
+
xmlParser.processChunk(choice.delta.content);
|
|
51
|
+
}
|
|
52
|
+
if (choice.delta.tool_calls) {
|
|
53
|
+
processToolCallDeltas(toolCallsByIndex, choice.delta.tool_calls);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (choice.finish_reason) {
|
|
57
|
+
aggregated.finish_reason = choice.finish_reason;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
export const aggregateChoice = () => (source) => new Observable((subscriber) => {
|
|
61
|
+
const aggregated = {};
|
|
62
|
+
const toolCallsByIndex = new Map();
|
|
63
|
+
const xmlParser = new InlineXmlParser();
|
|
64
|
+
const sub = source.subscribe({
|
|
65
|
+
next: (choice) => {
|
|
66
|
+
processChoice(choice, aggregated, toolCallsByIndex, xmlParser);
|
|
67
|
+
},
|
|
68
|
+
error: (err) => subscriber.error(err),
|
|
69
|
+
complete: () => {
|
|
70
|
+
const { content, tags } = xmlParser.finalize();
|
|
71
|
+
if (content) {
|
|
72
|
+
aggregated.delta = {
|
|
73
|
+
...aggregated.delta,
|
|
74
|
+
content,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (tags.length > 0) {
|
|
78
|
+
aggregated.thoughts = tags;
|
|
79
|
+
}
|
|
80
|
+
if (toolCallsByIndex.size > 0) {
|
|
81
|
+
aggregated.delta = {
|
|
82
|
+
...aggregated.delta,
|
|
83
|
+
tool_calls: finalizeToolCalls(toolCallsByIndex),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
subscriber.next(aggregated);
|
|
87
|
+
subscriber.complete();
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return () => sub.unsubscribe();
|
|
91
|
+
});
|
|
92
|
+
const mergeDetailsObject = (target, source) => {
|
|
93
|
+
if (!source)
|
|
94
|
+
return;
|
|
95
|
+
for (const [key, value] of Object.entries(source)) {
|
|
96
|
+
if (typeof value === 'number') {
|
|
97
|
+
target[key] = (target[key] || 0) + value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const buildAggregatedUsage = (promptTokens, completionTokens, totalTokens, cacheCreationInputTokens, cacheReadInputTokens, completionTokensDetails, promptTokensDetails) => {
|
|
102
|
+
const aggregated = {};
|
|
103
|
+
if (promptTokens > 0)
|
|
104
|
+
aggregated.prompt_tokens = promptTokens;
|
|
105
|
+
if (completionTokens > 0)
|
|
106
|
+
aggregated.completion_tokens = completionTokens;
|
|
107
|
+
if (totalTokens > 0)
|
|
108
|
+
aggregated.total_tokens = totalTokens;
|
|
109
|
+
if (cacheCreationInputTokens > 0)
|
|
110
|
+
aggregated.cache_creation_input_tokens = cacheCreationInputTokens;
|
|
111
|
+
if (cacheReadInputTokens > 0)
|
|
112
|
+
aggregated.cache_read_input_tokens = cacheReadInputTokens;
|
|
113
|
+
if (Object.keys(completionTokensDetails).length > 0) {
|
|
114
|
+
aggregated.completion_tokens_details = completionTokensDetails;
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
117
|
+
aggregated.prompt_tokens_details = promptTokensDetails;
|
|
118
|
+
}
|
|
119
|
+
return aggregated;
|
|
120
|
+
};
|
|
121
|
+
export const aggregateLLMUsage = () => (source) => new Observable((subscriber) => {
|
|
122
|
+
let promptTokens = 0;
|
|
123
|
+
let completionTokens = 0;
|
|
124
|
+
let totalTokens = 0;
|
|
125
|
+
let cacheCreationInputTokens = 0;
|
|
126
|
+
let cacheReadInputTokens = 0;
|
|
127
|
+
const completionTokensDetails = {};
|
|
128
|
+
const promptTokensDetails = {};
|
|
129
|
+
const sub = source.subscribe({
|
|
130
|
+
next: (usage) => {
|
|
131
|
+
if (usage.prompt_tokens)
|
|
132
|
+
promptTokens += usage.prompt_tokens;
|
|
133
|
+
if (usage.completion_tokens)
|
|
134
|
+
completionTokens += usage.completion_tokens;
|
|
135
|
+
if (usage.total_tokens)
|
|
136
|
+
totalTokens += usage.total_tokens;
|
|
137
|
+
if (usage.cache_creation_input_tokens)
|
|
138
|
+
cacheCreationInputTokens += usage.cache_creation_input_tokens;
|
|
139
|
+
if (usage.cache_read_input_tokens)
|
|
140
|
+
cacheReadInputTokens += usage.cache_read_input_tokens;
|
|
141
|
+
mergeDetailsObject(completionTokensDetails, usage.completion_tokens_details);
|
|
142
|
+
mergeDetailsObject(promptTokensDetails, usage.prompt_tokens_details);
|
|
143
|
+
},
|
|
144
|
+
error: (err) => subscriber.error(err),
|
|
145
|
+
complete: () => {
|
|
146
|
+
const aggregated = buildAggregatedUsage(promptTokens, completionTokens, totalTokens, cacheCreationInputTokens, cacheReadInputTokens, completionTokensDetails, promptTokensDetails);
|
|
147
|
+
subscriber.next(aggregated);
|
|
148
|
+
subscriber.complete();
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
return () => sub.unsubscribe();
|
|
152
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Observable } from 'rxjs';
|
|
2
|
+
import type { Choice, InlineXml } from './types';
|
|
3
|
+
type SplitResult = {
|
|
4
|
+
content: Observable<string>;
|
|
5
|
+
tags: Observable<InlineXml>;
|
|
6
|
+
};
|
|
7
|
+
export declare const getContent: <T extends Choice>() => import("rxjs").UnaryFunction<Observable<T>, Observable<string>>;
|
|
8
|
+
export declare const splitInlineXml: (source: Observable<string>) => SplitResult;
|
|
9
|
+
export declare const stripInlineXmlTags: (text: string) => string;
|
|
10
|
+
export declare class InlineXmlParser {
|
|
11
|
+
private buffer;
|
|
12
|
+
private prevEmittedWasTag;
|
|
13
|
+
private contentParts;
|
|
14
|
+
private extractedTags;
|
|
15
|
+
processChunk(chunk: string): void;
|
|
16
|
+
finalize(): {
|
|
17
|
+
content: string;
|
|
18
|
+
tags: InlineXml[];
|
|
19
|
+
};
|
|
20
|
+
private flushParsable;
|
|
21
|
+
private processTagHead;
|
|
22
|
+
private processPairedTag;
|
|
23
|
+
private emitContent;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { filter, map, pipe, ReplaySubject } from 'rxjs';
|
|
2
|
+
const parseAttributes = (attrsSrc) => {
|
|
3
|
+
const attrs = {};
|
|
4
|
+
const re = /([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>/]+)))?/g;
|
|
5
|
+
let m = re.exec(attrsSrc);
|
|
6
|
+
while (m !== null) {
|
|
7
|
+
const key = m[1];
|
|
8
|
+
const val = m[2] ?? m[3] ?? m[4] ?? '';
|
|
9
|
+
if (key in attrs) {
|
|
10
|
+
const prev = attrs[key];
|
|
11
|
+
attrs[key] = Array.isArray(prev) ? [...prev, val] : [prev, val];
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
attrs[key] = val;
|
|
15
|
+
}
|
|
16
|
+
m = re.exec(attrsSrc);
|
|
17
|
+
}
|
|
18
|
+
return attrs;
|
|
19
|
+
};
|
|
20
|
+
const emitIfNonEmpty = (s, prevWasTag, nextIsTagAhead, push) => {
|
|
21
|
+
let out = s;
|
|
22
|
+
if (prevWasTag)
|
|
23
|
+
out = out.replace(/^\s+/, '');
|
|
24
|
+
if (nextIsTagAhead)
|
|
25
|
+
out = out.replace(/\s+$/, '');
|
|
26
|
+
if (out.length > 0)
|
|
27
|
+
push(out);
|
|
28
|
+
};
|
|
29
|
+
export const getContent = () => pipe(filter((choice) => !!choice.delta?.content), map((choice) => choice.delta?.content));
|
|
30
|
+
export const splitInlineXml = (source) => {
|
|
31
|
+
const contentSubj = new ReplaySubject();
|
|
32
|
+
const tagsSubj = new ReplaySubject();
|
|
33
|
+
let buffer = '';
|
|
34
|
+
let prevEmittedWasTag = false;
|
|
35
|
+
const flushParsable = () => {
|
|
36
|
+
while (true) {
|
|
37
|
+
const lt = buffer.indexOf('<');
|
|
38
|
+
if (lt === -1) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const before = buffer.slice(0, lt);
|
|
42
|
+
const gt = buffer.indexOf('>', lt + 1);
|
|
43
|
+
if (gt === -1) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (before.length) {
|
|
47
|
+
emitIfNonEmpty(before, prevEmittedWasTag, true, (v) => contentSubj.next(v));
|
|
48
|
+
prevEmittedWasTag = false;
|
|
49
|
+
}
|
|
50
|
+
const tagHead = buffer.slice(lt + 1, gt).trim();
|
|
51
|
+
buffer = buffer.slice(gt + 1);
|
|
52
|
+
const selfClose = tagHead.endsWith('/');
|
|
53
|
+
const isClosing = tagHead.startsWith('/');
|
|
54
|
+
if (isClosing) {
|
|
55
|
+
prevEmittedWasTag = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const nameMatch = /^([A-Za-z_:][\w:.-]*)([\s\S]*)$/.exec(selfClose ? tagHead.slice(0, -1).trimEnd() : tagHead);
|
|
59
|
+
if (!nameMatch) {
|
|
60
|
+
prevEmittedWasTag = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const tagName = nameMatch[1];
|
|
64
|
+
const rawAttrs = nameMatch[2]?.trim() ?? '';
|
|
65
|
+
const attributes = parseAttributes(rawAttrs);
|
|
66
|
+
if (selfClose) {
|
|
67
|
+
tagsSubj.next({ name: tagName, attributes });
|
|
68
|
+
prevEmittedWasTag = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const closeSeq = `</${tagName}>`;
|
|
72
|
+
const closeIdx = buffer.indexOf(closeSeq);
|
|
73
|
+
if (closeIdx === -1) {
|
|
74
|
+
buffer = `<${tagHead}>${buffer}`;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const inner = buffer.slice(0, closeIdx);
|
|
78
|
+
buffer = buffer.slice(closeIdx + closeSeq.length);
|
|
79
|
+
tagsSubj.next({ name: tagName, content: inner, attributes });
|
|
80
|
+
prevEmittedWasTag = true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const subscription = source.subscribe({
|
|
84
|
+
next: (chunk) => {
|
|
85
|
+
buffer += chunk;
|
|
86
|
+
flushParsable();
|
|
87
|
+
const nextLt = buffer.indexOf('<');
|
|
88
|
+
if (nextLt === -1 && buffer.length) {
|
|
89
|
+
let out = buffer;
|
|
90
|
+
if (prevEmittedWasTag)
|
|
91
|
+
out = out.replace(/^\s+/, '');
|
|
92
|
+
if (out.length)
|
|
93
|
+
contentSubj.next(out);
|
|
94
|
+
buffer = '';
|
|
95
|
+
prevEmittedWasTag = false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
error: (err) => {
|
|
99
|
+
try {
|
|
100
|
+
contentSubj.error(err);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
tagsSubj.error(err);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
complete: () => {
|
|
107
|
+
if (buffer.length) {
|
|
108
|
+
let out = buffer;
|
|
109
|
+
if (prevEmittedWasTag)
|
|
110
|
+
out = out.replace(/^\s+/, '');
|
|
111
|
+
if (out.length)
|
|
112
|
+
contentSubj.next(out);
|
|
113
|
+
buffer = '';
|
|
114
|
+
}
|
|
115
|
+
contentSubj.complete();
|
|
116
|
+
tagsSubj.complete();
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const finalize = () => subscription.unsubscribe();
|
|
120
|
+
contentSubj.subscribe({ complete: finalize, error: finalize });
|
|
121
|
+
tagsSubj.subscribe({ complete: finalize, error: finalize });
|
|
122
|
+
return { content: contentSubj.asObservable(), tags: tagsSubj.asObservable() };
|
|
123
|
+
};
|
|
124
|
+
export const stripInlineXmlTags = (text) => {
|
|
125
|
+
let result = text;
|
|
126
|
+
result = result.replace(/<([A-Za-z_:][\w:.-]*)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g, ' ');
|
|
127
|
+
result = result.replace(/<([A-Za-z_:][\w:.-]*)(?:\s[^>]*)?\s*\/>/g, ' ');
|
|
128
|
+
result = result.replace(/\s+/g, ' ').trim();
|
|
129
|
+
return result;
|
|
130
|
+
};
|
|
131
|
+
export class InlineXmlParser {
|
|
132
|
+
buffer = '';
|
|
133
|
+
prevEmittedWasTag = false;
|
|
134
|
+
contentParts = [];
|
|
135
|
+
extractedTags = [];
|
|
136
|
+
processChunk(chunk) {
|
|
137
|
+
this.buffer += chunk;
|
|
138
|
+
this.flushParsable();
|
|
139
|
+
const nextLt = this.buffer.indexOf('<');
|
|
140
|
+
if (nextLt === -1 && this.buffer.length) {
|
|
141
|
+
let out = this.buffer;
|
|
142
|
+
if (this.prevEmittedWasTag)
|
|
143
|
+
out = out.replace(/^\s+/, '');
|
|
144
|
+
if (out.length)
|
|
145
|
+
this.contentParts.push(out);
|
|
146
|
+
this.buffer = '';
|
|
147
|
+
this.prevEmittedWasTag = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finalize() {
|
|
151
|
+
if (this.buffer.length) {
|
|
152
|
+
let out = this.buffer;
|
|
153
|
+
if (this.prevEmittedWasTag)
|
|
154
|
+
out = out.replace(/^\s+/, '');
|
|
155
|
+
if (out.length)
|
|
156
|
+
this.contentParts.push(out);
|
|
157
|
+
this.buffer = '';
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
content: this.contentParts.join(''),
|
|
161
|
+
tags: this.extractedTags,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
flushParsable() {
|
|
165
|
+
while (true) {
|
|
166
|
+
const lt = this.buffer.indexOf('<');
|
|
167
|
+
if (lt === -1) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const gt = this.buffer.indexOf('>', lt + 1);
|
|
171
|
+
if (gt === -1) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (lt > 0) {
|
|
175
|
+
this.emitContent(this.buffer.slice(0, lt), true);
|
|
176
|
+
}
|
|
177
|
+
const tagHead = this.buffer.slice(lt + 1, gt).trim();
|
|
178
|
+
this.buffer = this.buffer.slice(gt + 1);
|
|
179
|
+
if (!this.processTagHead(tagHead)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
processTagHead(tagHead) {
|
|
185
|
+
const selfClose = tagHead.endsWith('/');
|
|
186
|
+
const isClosing = tagHead.startsWith('/');
|
|
187
|
+
if (isClosing) {
|
|
188
|
+
this.prevEmittedWasTag = true;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
const nameMatch = /^([A-Za-z_:][\w:.-]*)([\s\S]*)$/.exec(selfClose ? tagHead.slice(0, -1).trimEnd() : tagHead);
|
|
192
|
+
if (!nameMatch) {
|
|
193
|
+
this.prevEmittedWasTag = true;
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const tagName = nameMatch[1];
|
|
197
|
+
const attributes = parseAttributes(nameMatch[2]?.trim() ?? '');
|
|
198
|
+
if (selfClose) {
|
|
199
|
+
this.extractedTags.push({ name: tagName, attributes });
|
|
200
|
+
this.prevEmittedWasTag = true;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return this.processPairedTag(tagName, tagHead, attributes);
|
|
204
|
+
}
|
|
205
|
+
processPairedTag(tagName, tagHead, attributes) {
|
|
206
|
+
const closeSeq = `</${tagName}>`;
|
|
207
|
+
const closeIdx = this.buffer.indexOf(closeSeq);
|
|
208
|
+
if (closeIdx === -1) {
|
|
209
|
+
this.buffer = `<${tagHead}>${this.buffer}`;
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const inner = this.buffer.slice(0, closeIdx);
|
|
213
|
+
this.buffer = this.buffer.slice(closeIdx + closeSeq.length);
|
|
214
|
+
this.extractedTags.push({ name: tagName, content: inner, attributes });
|
|
215
|
+
this.prevEmittedWasTag = true;
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
emitContent(s, nextIsTagAhead) {
|
|
219
|
+
let out = s;
|
|
220
|
+
if (this.prevEmittedWasTag)
|
|
221
|
+
out = out.replace(/^\s+/, '');
|
|
222
|
+
if (nextIsTagAhead)
|
|
223
|
+
out = out.replace(/\s+$/, '');
|
|
224
|
+
if (out.length > 0) {
|
|
225
|
+
this.contentParts.push(out);
|
|
226
|
+
this.prevEmittedWasTag = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|