@obsunified/collector 2.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 +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0002 Stage 3 + 5 — OpenAI provider adapter.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `lib/llm.ts` (Anthropic) and the tool-use loop in `lib/ask.ts`,
|
|
5
|
+
* but talks to OpenAI's /chat/completions (function-calling) shape:
|
|
6
|
+
*
|
|
7
|
+
* - Auth: `Authorization: Bearer <key>`
|
|
8
|
+
* - Messages: { role: "system" | "user" | "assistant" | "tool", content }
|
|
9
|
+
* - Tools: top-level `tools: [{ type: "function", function: { name, description, parameters } }]`
|
|
10
|
+
* - Model emits `message.tool_calls: [{ id, type: "function", function: { name, arguments: "<json string>" } }]`
|
|
11
|
+
* - Tool results: `{ role: "tool", tool_call_id, content }`
|
|
12
|
+
* - Stop: `finish_reason === "stop"` for end_turn, `"tool_calls"` for tool use
|
|
13
|
+
*
|
|
14
|
+
* Same system prompts as Anthropic — the rendering rules don't change.
|
|
15
|
+
* Same NarrativeRequest / AskRunDeps inputs so the dispatcher in
|
|
16
|
+
* `lib/llm.ts` and `lib/ask.ts` is one-line.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
AnalysisDefinition,
|
|
21
|
+
AnalysisResult,
|
|
22
|
+
AskEvidence,
|
|
23
|
+
AskQuery,
|
|
24
|
+
AskResponse,
|
|
25
|
+
} from "@obsunified/types";
|
|
26
|
+
import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
|
|
27
|
+
import type { AskRunDeps } from "./ask";
|
|
28
|
+
import { askEvidenceReferences } from "./evidence-references";
|
|
29
|
+
import type { LlmConfig, NarrativeRequest } from "./llm";
|
|
30
|
+
import { LlmCallError } from "./llm";
|
|
31
|
+
|
|
32
|
+
const DEFAULT_API_URL = "https://api.openai.com/v1";
|
|
33
|
+
|
|
34
|
+
const NARRATIVE_SYSTEM_PROMPT = `You write one-sentence operational narratives for a self-hosted observability dashboard.
|
|
35
|
+
|
|
36
|
+
Hard rules — violations are unusable:
|
|
37
|
+
1. Never start with "I", "Here's", "We", "It looks", or any first-person/chatbot framing. Write declarative statements about the system.
|
|
38
|
+
2. If you reference a trace, span, log, or service, include the supplied identifier inline (e.g. "trace abc12345").
|
|
39
|
+
3. Include a time anchor: "starting Nm ago", "for the last Nm", "since HH:MM" — pull from the inputs.
|
|
40
|
+
4. ≤ 2 sentences total, ≤ 220 characters. Numbers belong in the sentence, not in a separate label.
|
|
41
|
+
5. If the inputs are insufficient to write a useful sentence, output exactly the string NO_NARRATIVE (no quotes).
|
|
42
|
+
|
|
43
|
+
Tone: like a senior on-call writing a Slack message. Specific, blunt, no hedging, no advice ("you should..."), no questions.`;
|
|
44
|
+
|
|
45
|
+
const ASK_SYSTEM_PROMPT = `You answer operational questions about a self-hosted observability dashboard's telemetry. Use the supplied tools — do not invent numbers.
|
|
46
|
+
|
|
47
|
+
Workflow:
|
|
48
|
+
1. Use list_analyses to see what's available. Filter by group when it helps.
|
|
49
|
+
2. Pick the analyses most likely to answer the question. Call run_analysis on each (typically 1–3, max 5).
|
|
50
|
+
3. Read the results. Synthesize a 1–2 sentence answer.
|
|
51
|
+
|
|
52
|
+
Hard rules — violations are unusable:
|
|
53
|
+
1. Never start with "I", "Here's", "We", or any chatbot framing. Write declarative statements about the system.
|
|
54
|
+
2. Cite the analyses you consulted by id, e.g. "(latency_p95_overall)".
|
|
55
|
+
3. Include a time anchor when describing a number ("in the last 5 minutes", "vs the prior hour", "for the past 24h").
|
|
56
|
+
4. ≤ 2 sentences, ≤ 280 characters total. Numbers belong in the sentence.
|
|
57
|
+
5. If the available analyses don't cover the question, say exactly that and name the closest analysis you found.
|
|
58
|
+
|
|
59
|
+
Output: write only the final answer text. No preamble, no JSON.`;
|
|
60
|
+
|
|
61
|
+
const OPENAI_TOOLS = [
|
|
62
|
+
{
|
|
63
|
+
type: "function" as const,
|
|
64
|
+
function: {
|
|
65
|
+
name: "list_analyses",
|
|
66
|
+
description:
|
|
67
|
+
"List registered analyses for the current project. Optionally filter by group (Health, Services, Dependencies, Async, AI, Frontend, Custom). Returns id, title, group, source, view, and scope (when set).",
|
|
68
|
+
parameters: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
group: { type: "string" },
|
|
72
|
+
view: { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "function" as const,
|
|
79
|
+
function: {
|
|
80
|
+
name: "run_analysis",
|
|
81
|
+
description:
|
|
82
|
+
"Read the latest result for one analysis by id. Returns status, primary value, baseline, delta_pct, and the analysis-specific payload.",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
id: { type: "string" },
|
|
87
|
+
},
|
|
88
|
+
required: ["id"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
interface OpenAiToolCall {
|
|
95
|
+
id: string;
|
|
96
|
+
type: "function";
|
|
97
|
+
function: { name: string; arguments: string };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface OpenAiMessage {
|
|
101
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
102
|
+
content?: string | null;
|
|
103
|
+
tool_calls?: OpenAiToolCall[];
|
|
104
|
+
tool_call_id?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface OpenAiChoice {
|
|
108
|
+
index?: number;
|
|
109
|
+
message?: OpenAiMessage;
|
|
110
|
+
finish_reason?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface OpenAiResponse {
|
|
114
|
+
choices?: OpenAiChoice[];
|
|
115
|
+
usage?: {
|
|
116
|
+
prompt_tokens?: number;
|
|
117
|
+
completion_tokens?: number;
|
|
118
|
+
total_tokens?: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const baseUrl = (cfg: LlmConfig) =>
|
|
123
|
+
(cfg.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
124
|
+
|
|
125
|
+
interface PostMeta {
|
|
126
|
+
maxTokens?: number;
|
|
127
|
+
turnIndex?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const post = async <T extends { usage?: OpenAiResponse["usage"] }>(
|
|
131
|
+
cfg: LlmConfig,
|
|
132
|
+
path: string,
|
|
133
|
+
body: unknown,
|
|
134
|
+
meta: PostMeta = {},
|
|
135
|
+
): Promise<T> => {
|
|
136
|
+
const url = `${baseUrl(cfg)}${path}`;
|
|
137
|
+
const exec = async (
|
|
138
|
+
span: { setAttribute(k: string, v: unknown): void } | null,
|
|
139
|
+
): Promise<T> => {
|
|
140
|
+
const response = await fetch(url, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
144
|
+
"content-type": "application/json",
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
signal: cfg.signal,
|
|
148
|
+
});
|
|
149
|
+
if (span) span.setAttribute("http.response.status_code", response.status);
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const text = await response.text().catch(() => "");
|
|
152
|
+
throw new LlmCallError(
|
|
153
|
+
`openai ${response.status}: ${text.slice(0, 200)}`,
|
|
154
|
+
response.status,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const json = (await response.json()) as T;
|
|
158
|
+
if (span && json.usage) {
|
|
159
|
+
if (typeof json.usage.prompt_tokens === "number")
|
|
160
|
+
span.setAttribute(
|
|
161
|
+
"gen_ai.usage.input_tokens",
|
|
162
|
+
json.usage.prompt_tokens,
|
|
163
|
+
);
|
|
164
|
+
if (typeof json.usage.completion_tokens === "number")
|
|
165
|
+
span.setAttribute(
|
|
166
|
+
"gen_ai.usage.output_tokens",
|
|
167
|
+
json.usage.completion_tokens,
|
|
168
|
+
);
|
|
169
|
+
if (typeof json.usage.total_tokens === "number")
|
|
170
|
+
span.setAttribute("gen_ai.usage.total_tokens", json.usage.total_tokens);
|
|
171
|
+
}
|
|
172
|
+
return json;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (!cfg.tracer) return exec(null);
|
|
176
|
+
const attrs: Record<string, unknown> = {
|
|
177
|
+
"openinference.span.kind": "LLM",
|
|
178
|
+
"gen_ai.system": "openai",
|
|
179
|
+
"gen_ai.request.model": cfg.model,
|
|
180
|
+
"http.url": url,
|
|
181
|
+
};
|
|
182
|
+
if (typeof meta.maxTokens === "number")
|
|
183
|
+
attrs["gen_ai.request.max_tokens"] = meta.maxTokens;
|
|
184
|
+
if (typeof meta.turnIndex === "number") attrs["llm.turn"] = meta.turnIndex;
|
|
185
|
+
return cfg.tracer("llm.openai.chat", async (span) => exec(span), attrs);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ── narrative (single-shot) ────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const renderTemplate = (
|
|
191
|
+
template: string,
|
|
192
|
+
def: AnalysisDefinition,
|
|
193
|
+
r: AnalysisResult,
|
|
194
|
+
): string => {
|
|
195
|
+
const scope = (def.scope ?? {}) as Record<string, unknown>;
|
|
196
|
+
const traceIds = Array.isArray(r.payload?.trace_ids)
|
|
197
|
+
? (r.payload.trace_ids as unknown[])
|
|
198
|
+
.filter((x): x is string => typeof x === "string")
|
|
199
|
+
.slice(0, 5)
|
|
200
|
+
.join(", ")
|
|
201
|
+
: "";
|
|
202
|
+
const subs: Record<string, string> = {
|
|
203
|
+
title: def.title,
|
|
204
|
+
status: r.status,
|
|
205
|
+
primary: r.primaryValue === null ? "n/a" : String(r.primaryValue),
|
|
206
|
+
baseline: r.baselineValue === null ? "n/a" : String(r.baselineValue),
|
|
207
|
+
delta_pct: r.deltaPct === null ? "n/a" : `${r.deltaPct.toFixed(1)}%`,
|
|
208
|
+
service: typeof scope.service === "string" ? scope.service : "",
|
|
209
|
+
trace_ids: traceIds,
|
|
210
|
+
};
|
|
211
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, k) => subs[k] ?? "");
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const buildNarrativeUserPrompt = ({
|
|
215
|
+
definition,
|
|
216
|
+
current,
|
|
217
|
+
previous,
|
|
218
|
+
}: NarrativeRequest): string => {
|
|
219
|
+
const lines: string[] = [];
|
|
220
|
+
const tmplRendered = renderTemplate(
|
|
221
|
+
definition.narrate?.prompt ?? "",
|
|
222
|
+
definition,
|
|
223
|
+
current,
|
|
224
|
+
).trim();
|
|
225
|
+
if (tmplRendered) lines.push(tmplRendered);
|
|
226
|
+
|
|
227
|
+
lines.push("", "Current state:");
|
|
228
|
+
lines.push(`- panel: ${definition.title}`);
|
|
229
|
+
lines.push(`- status: ${current.status}`);
|
|
230
|
+
if (current.primaryValue !== null)
|
|
231
|
+
lines.push(`- primary: ${current.primaryValue}`);
|
|
232
|
+
if (current.baselineValue !== null)
|
|
233
|
+
lines.push(`- baseline (1h): ${current.baselineValue}`);
|
|
234
|
+
if (current.deltaPct !== null)
|
|
235
|
+
lines.push(`- delta vs baseline: ${current.deltaPct.toFixed(1)}%`);
|
|
236
|
+
|
|
237
|
+
const since = previous
|
|
238
|
+
? Math.max(
|
|
239
|
+
1,
|
|
240
|
+
Math.round(
|
|
241
|
+
(new Date(current.generatedAt).getTime() -
|
|
242
|
+
new Date(previous.generatedAt).getTime()) /
|
|
243
|
+
60000,
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
: null;
|
|
247
|
+
if (since !== null) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`- previous run was ${since} minute${since === 1 ? "" : "s"} ago, status=${previous?.status ?? "n/a"}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const payloadKeys = Object.keys(current.payload ?? {}).filter(
|
|
254
|
+
(k) => k !== "sparkline",
|
|
255
|
+
);
|
|
256
|
+
if (payloadKeys.length > 0) {
|
|
257
|
+
lines.push("", "Payload details:");
|
|
258
|
+
for (const k of payloadKeys) {
|
|
259
|
+
const v = current.payload[k];
|
|
260
|
+
const rendered =
|
|
261
|
+
typeof v === "string" || typeof v === "number" || typeof v === "boolean"
|
|
262
|
+
? String(v)
|
|
263
|
+
: JSON.stringify(v);
|
|
264
|
+
if (rendered.length <= 200) lines.push(`- ${k}: ${rendered}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
lines.push(
|
|
268
|
+
"",
|
|
269
|
+
"Output: a single sentence (max two), no preamble, no quotes, no Markdown.",
|
|
270
|
+
);
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export async function generateNarrativeOpenAI(
|
|
275
|
+
req: NarrativeRequest,
|
|
276
|
+
config: LlmConfig,
|
|
277
|
+
): Promise<string | null> {
|
|
278
|
+
const body = await post<OpenAiResponse>(
|
|
279
|
+
config,
|
|
280
|
+
"/chat/completions",
|
|
281
|
+
{
|
|
282
|
+
model: config.model,
|
|
283
|
+
messages: [
|
|
284
|
+
{ role: "system", content: NARRATIVE_SYSTEM_PROMPT },
|
|
285
|
+
{ role: "user", content: buildNarrativeUserPrompt(req) },
|
|
286
|
+
],
|
|
287
|
+
max_tokens: 200,
|
|
288
|
+
},
|
|
289
|
+
{ maxTokens: 200 },
|
|
290
|
+
);
|
|
291
|
+
const text = (body.choices?.[0]?.message?.content ?? "").trim();
|
|
292
|
+
if (body.choices?.[0]?.finish_reason === "length") {
|
|
293
|
+
throw new LlmCallError("openai response truncated by max_tokens");
|
|
294
|
+
}
|
|
295
|
+
if (!text || text === "NO_NARRATIVE") return null;
|
|
296
|
+
return text.replace(/^["']|["']$/g, "").trim();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── ask (tool-use loop) ────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
const MAX_ITERATIONS = 5;
|
|
302
|
+
|
|
303
|
+
const summarizeDefinition = (def: AnalysisDefinition) => ({
|
|
304
|
+
id: def.id,
|
|
305
|
+
title: def.title,
|
|
306
|
+
group: def.group,
|
|
307
|
+
source: def.source,
|
|
308
|
+
view: def.view,
|
|
309
|
+
scope: def.scope ?? null,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const summarizeResult = (
|
|
313
|
+
def: AnalysisDefinition,
|
|
314
|
+
result: AnalysisResult | null,
|
|
315
|
+
) => {
|
|
316
|
+
if (!result) {
|
|
317
|
+
return {
|
|
318
|
+
definition: summarizeDefinition(def),
|
|
319
|
+
result: null,
|
|
320
|
+
note: "No result yet — analysis hasn't run.",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
definition: summarizeDefinition(def),
|
|
325
|
+
result: {
|
|
326
|
+
generatedAt: result.generatedAt,
|
|
327
|
+
status: result.status,
|
|
328
|
+
primaryValue: result.primaryValue,
|
|
329
|
+
baselineValue: result.baselineValue,
|
|
330
|
+
deltaPct: result.deltaPct,
|
|
331
|
+
payload: result.payload,
|
|
332
|
+
narrative: result.narrative,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export async function runAskOpenAI(
|
|
338
|
+
question: string,
|
|
339
|
+
deps: AskRunDeps,
|
|
340
|
+
): Promise<AskResponse> {
|
|
341
|
+
const startedAt = new Date().toISOString();
|
|
342
|
+
const queries: AskQuery[] = [];
|
|
343
|
+
const evidence = new Map<string, AskEvidence>();
|
|
344
|
+
|
|
345
|
+
const messages: OpenAiMessage[] = [
|
|
346
|
+
{ role: "system", content: ASK_SYSTEM_PROMPT },
|
|
347
|
+
{ role: "user", content: question },
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
for (let i = 0; i < MAX_ITERATIONS; i += 1) {
|
|
351
|
+
const body = await post<OpenAiResponse>(
|
|
352
|
+
deps.llm,
|
|
353
|
+
"/chat/completions",
|
|
354
|
+
{
|
|
355
|
+
model: deps.llm.model,
|
|
356
|
+
messages,
|
|
357
|
+
tools: OPENAI_TOOLS,
|
|
358
|
+
max_tokens: 1024,
|
|
359
|
+
},
|
|
360
|
+
{ maxTokens: 1024, turnIndex: i },
|
|
361
|
+
);
|
|
362
|
+
const choice = body.choices?.[0];
|
|
363
|
+
const message = choice?.message;
|
|
364
|
+
const toolCalls = message?.tool_calls ?? [];
|
|
365
|
+
const finishReason = choice?.finish_reason;
|
|
366
|
+
const text = (message?.content ?? "").trim();
|
|
367
|
+
|
|
368
|
+
// Final answer.
|
|
369
|
+
if (
|
|
370
|
+
(!toolCalls || toolCalls.length === 0) &&
|
|
371
|
+
(finishReason === "stop" || finishReason === undefined)
|
|
372
|
+
) {
|
|
373
|
+
return {
|
|
374
|
+
answer: text.length > 0 ? text : null,
|
|
375
|
+
evidence: [...evidence.values()],
|
|
376
|
+
evidenceReferences: askEvidenceReferences(evidence.values()),
|
|
377
|
+
evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
|
|
378
|
+
queries,
|
|
379
|
+
error: text.length > 0 ? null : "model returned no text",
|
|
380
|
+
timestamp: startedAt,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Append assistant message verbatim — must include tool_calls for the
|
|
385
|
+
// next turn's tool result messages to be valid.
|
|
386
|
+
messages.push({
|
|
387
|
+
role: "assistant",
|
|
388
|
+
content: message?.content ?? null,
|
|
389
|
+
tool_calls: toolCalls,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Execute each requested tool.
|
|
393
|
+
for (const call of toolCalls) {
|
|
394
|
+
const t0 = Date.now();
|
|
395
|
+
let resultPayload: unknown;
|
|
396
|
+
let parsedArgs: Record<string, unknown> = {};
|
|
397
|
+
try {
|
|
398
|
+
parsedArgs = call.function.arguments
|
|
399
|
+
? (JSON.parse(call.function.arguments) as Record<string, unknown>)
|
|
400
|
+
: {};
|
|
401
|
+
} catch {
|
|
402
|
+
parsedArgs = {};
|
|
403
|
+
}
|
|
404
|
+
if ((!toolCalls || toolCalls.length === 0) && finishReason === "length") {
|
|
405
|
+
return {
|
|
406
|
+
answer: null,
|
|
407
|
+
evidence: [...evidence.values()],
|
|
408
|
+
evidenceReferences: askEvidenceReferences(evidence.values()),
|
|
409
|
+
evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
|
|
410
|
+
queries,
|
|
411
|
+
error: "model response was truncated by max_tokens",
|
|
412
|
+
timestamp: startedAt,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const toolName = call.function.name;
|
|
416
|
+
const tracer = deps.llm.tracer;
|
|
417
|
+
const dispatchTool = async (
|
|
418
|
+
span: { setAttribute(k: string, v: unknown): void } | null,
|
|
419
|
+
): Promise<unknown> => {
|
|
420
|
+
if (toolName === "list_analyses") {
|
|
421
|
+
const all = await deps.listAnalyses({
|
|
422
|
+
group:
|
|
423
|
+
typeof parsedArgs.group === "string"
|
|
424
|
+
? parsedArgs.group
|
|
425
|
+
: undefined,
|
|
426
|
+
view:
|
|
427
|
+
typeof parsedArgs.view === "string" ? parsedArgs.view : undefined,
|
|
428
|
+
});
|
|
429
|
+
const out = all.slice(0, 80).map(summarizeDefinition);
|
|
430
|
+
if (span) span.setAttribute("tool.result_count", out.length);
|
|
431
|
+
queries.push({
|
|
432
|
+
tool: "list_analyses",
|
|
433
|
+
args: parsedArgs,
|
|
434
|
+
durationMs: Date.now() - t0,
|
|
435
|
+
});
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
if (toolName === "run_analysis") {
|
|
439
|
+
const id = typeof parsedArgs.id === "string" ? parsedArgs.id : "";
|
|
440
|
+
if (!id) {
|
|
441
|
+
if (span) span.setAttribute("tool.outcome", "missing_id");
|
|
442
|
+
queries.push({
|
|
443
|
+
tool: "run_analysis",
|
|
444
|
+
args: parsedArgs,
|
|
445
|
+
durationMs: Date.now() - t0,
|
|
446
|
+
});
|
|
447
|
+
return { error: "id is required" };
|
|
448
|
+
}
|
|
449
|
+
const found = await deps.getLatestResult(id);
|
|
450
|
+
queries.push({
|
|
451
|
+
tool: "run_analysis",
|
|
452
|
+
args: parsedArgs,
|
|
453
|
+
durationMs: Date.now() - t0,
|
|
454
|
+
});
|
|
455
|
+
if (!found) {
|
|
456
|
+
if (span) span.setAttribute("tool.outcome", "not_found");
|
|
457
|
+
return { error: `analysis ${id} not found` };
|
|
458
|
+
}
|
|
459
|
+
if (span) {
|
|
460
|
+
span.setAttribute("tool.outcome", "ok");
|
|
461
|
+
span.setAttribute("analysis.id", id);
|
|
462
|
+
if (found.result) {
|
|
463
|
+
span.setAttribute("analysis.status", found.result.status);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
evidence.set(id, {
|
|
467
|
+
analysisId: id,
|
|
468
|
+
definition: found.definition,
|
|
469
|
+
result: found.result,
|
|
470
|
+
});
|
|
471
|
+
return summarizeResult(found.definition, found.result);
|
|
472
|
+
}
|
|
473
|
+
if (span) span.setAttribute("tool.outcome", "unknown_tool");
|
|
474
|
+
return { error: `unknown tool: ${toolName}` };
|
|
475
|
+
};
|
|
476
|
+
try {
|
|
477
|
+
if (tracer) {
|
|
478
|
+
resultPayload = await tracer(
|
|
479
|
+
`tool.${toolName}`,
|
|
480
|
+
async (span) => dispatchTool(span),
|
|
481
|
+
{
|
|
482
|
+
"openinference.span.kind": "TOOL",
|
|
483
|
+
"tool.name": toolName,
|
|
484
|
+
"tool.args": JSON.stringify(parsedArgs).slice(0, 512),
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
resultPayload = await dispatchTool(null);
|
|
489
|
+
}
|
|
490
|
+
} catch (err) {
|
|
491
|
+
resultPayload = {
|
|
492
|
+
error: err instanceof Error ? err.message : String(err),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
messages.push({
|
|
496
|
+
role: "tool",
|
|
497
|
+
tool_call_id: call.id,
|
|
498
|
+
content: JSON.stringify(resultPayload),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
answer: null,
|
|
505
|
+
evidence: [...evidence.values()],
|
|
506
|
+
evidenceReferences: askEvidenceReferences(evidence.values()),
|
|
507
|
+
evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
|
|
508
|
+
queries,
|
|
509
|
+
error: `iteration cap (${MAX_ITERATIONS}) reached without final answer`,
|
|
510
|
+
timestamp: startedAt,
|
|
511
|
+
};
|
|
512
|
+
}
|