@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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0002 — HTTP surface for application-aware Analyses.
|
|
3
|
+
*
|
|
4
|
+
* GET /internal/analyses — list registered definitions
|
|
5
|
+
* GET /internal/analyses/:id/result — definition + latest result
|
|
6
|
+
* GET /internal/analyses/results — bulk: every definition + latest
|
|
7
|
+
* POST /internal/analyses/:id/run — Stage 4: on-demand re-run for
|
|
8
|
+
* view: "page" investigations
|
|
9
|
+
*
|
|
10
|
+
* Cron writes happen via the scheduled handler (see analyses-runner.ts).
|
|
11
|
+
* The on-demand POST exists so an investigation page's [Re-run] button
|
|
12
|
+
* doesn't have to wait for the next 5-min tick.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
AnalysesListResponse,
|
|
17
|
+
AnalysisResultResponse,
|
|
18
|
+
AnalysisResultsBulkResponse,
|
|
19
|
+
} from "@obsunified/types";
|
|
20
|
+
import { getConfiguredRetentionHours } from "@obsunified/types/constants";
|
|
21
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
22
|
+
import { runSqlAnalysis } from "../lib/analyses-runner";
|
|
23
|
+
import { AnalysesStore } from "../lib/analyses-store";
|
|
24
|
+
import { sqlDbFor } from "../lib/sql-db";
|
|
25
|
+
import { getProjectId } from "./_context";
|
|
26
|
+
|
|
27
|
+
export const analysesRoutesPlugin: CollectorPlugin = {
|
|
28
|
+
name: "analyses-routes",
|
|
29
|
+
register(app, runtime) {
|
|
30
|
+
app.get("/internal/analyses", async (c) => {
|
|
31
|
+
const projectId = getProjectId(c);
|
|
32
|
+
const store = new AnalysesStore(sqlDbFor(c.env));
|
|
33
|
+
const analyses = await store.listDefinitions(projectId);
|
|
34
|
+
const response: AnalysesListResponse = {
|
|
35
|
+
analyses,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
return c.json(response);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.get("/internal/analyses/results", async (c) => {
|
|
42
|
+
const projectId = getProjectId(c);
|
|
43
|
+
const store = new AnalysesStore(sqlDbFor(c.env));
|
|
44
|
+
const definitions = await store.listDefinitions(projectId);
|
|
45
|
+
const ids = definitions.map((def) => def.id);
|
|
46
|
+
const latest = await store.getLatestResultsBulk(projectId, ids);
|
|
47
|
+
const response: AnalysisResultsBulkResponse = {
|
|
48
|
+
results: definitions.map((definition) => ({
|
|
49
|
+
definition,
|
|
50
|
+
result: latest.get(definition.id) ?? null,
|
|
51
|
+
})),
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
return c.json(response);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Stage 4 [Re-run] target. Re-executes the SQL right now and persists
|
|
58
|
+
// the new result. We deliberately keep this narrow:
|
|
59
|
+
// - only `view: "page"` definitions (tile cron is fast enough)
|
|
60
|
+
// - no narrate pass on the on-demand path; the page UI uses the most
|
|
61
|
+
// recent narrative from the cron tick (cheaper, avoids surprise
|
|
62
|
+
// LLM calls from a button click)
|
|
63
|
+
app.post("/internal/analyses/:id/run", async (c) => {
|
|
64
|
+
const projectId = getProjectId(c);
|
|
65
|
+
const id = c.req.param("id");
|
|
66
|
+
if (!id) {
|
|
67
|
+
return c.json({ error: "Bad Request", message: "id is required" }, 400);
|
|
68
|
+
}
|
|
69
|
+
const store = new AnalysesStore(sqlDbFor(c.env));
|
|
70
|
+
const definitions = await store.listDefinitions(projectId);
|
|
71
|
+
const definition = definitions.find((def) => def.id === id);
|
|
72
|
+
if (!definition) {
|
|
73
|
+
return c.json(
|
|
74
|
+
{ error: "Not Found", message: `Analysis ${id} not found` },
|
|
75
|
+
404,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (definition.view !== "page") {
|
|
79
|
+
return c.json(
|
|
80
|
+
{
|
|
81
|
+
error: "Bad Request",
|
|
82
|
+
message: "on-demand run only supported for view=page analyses",
|
|
83
|
+
},
|
|
84
|
+
400,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const retentionHours = getConfiguredRetentionHours(c.env.RETENTION_HOURS);
|
|
88
|
+
const expiresAt = Date.now() + retentionHours * 3600 * 1000;
|
|
89
|
+
try {
|
|
90
|
+
const result = await runSqlAnalysis(definition, {
|
|
91
|
+
db: sqlDbFor(c.env),
|
|
92
|
+
projectId,
|
|
93
|
+
retentionHours,
|
|
94
|
+
});
|
|
95
|
+
await store.insertResult(result, expiresAt);
|
|
96
|
+
await store.markRan(projectId, definition.id, result.generatedAt);
|
|
97
|
+
const response: AnalysisResultResponse = {
|
|
98
|
+
definition,
|
|
99
|
+
result,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
return c.json(response);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
runtime.logger.error("[analyses] on-demand run failed", {
|
|
106
|
+
analysis_id: id,
|
|
107
|
+
project_id: projectId,
|
|
108
|
+
error: message,
|
|
109
|
+
});
|
|
110
|
+
return c.json({ error: "Internal Server Error", message }, 500);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.get("/internal/analyses/:id/result", async (c) => {
|
|
115
|
+
const projectId = getProjectId(c);
|
|
116
|
+
const id = c.req.param("id");
|
|
117
|
+
if (!id) {
|
|
118
|
+
return c.json({ error: "Bad Request", message: "id is required" }, 400);
|
|
119
|
+
}
|
|
120
|
+
const store = new AnalysesStore(sqlDbFor(c.env));
|
|
121
|
+
const definitions = await store.listDefinitions(projectId);
|
|
122
|
+
const definition = definitions.find((def) => def.id === id);
|
|
123
|
+
if (!definition) {
|
|
124
|
+
return c.json(
|
|
125
|
+
{ error: "Not Found", message: `Analysis ${id} not found` },
|
|
126
|
+
404,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const result = await store.getLatestResult(projectId, id);
|
|
130
|
+
const response: AnalysisResultResponse = {
|
|
131
|
+
definition,
|
|
132
|
+
result,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
return c.json(response);
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0002 Stage 5 — Ask box HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* POST /internal/ask { question } → AskResponse
|
|
5
|
+
*
|
|
6
|
+
* The dashboard's top-bar AskBox calls this once per question. Single-turn
|
|
7
|
+
* for v1; the conversational `/ask` thread (with follow-ups + "Save as
|
|
8
|
+
* panel") lands in a follow-up commit.
|
|
9
|
+
*
|
|
10
|
+
* The route does no SQL itself — it composes runAsk() from the catalog
|
|
11
|
+
* lookups (AnalysesStore + getAllAnalysesForProject) and the LLM config.
|
|
12
|
+
* If ANTHROPIC_API_KEY is unset we return a structured error rather than
|
|
13
|
+
* 500-ing, so the dashboard can render a "set ANTHROPIC_API_KEY to
|
|
14
|
+
* enable Ask" hint instead of a generic failure.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AskRequest, AskResponse } from "@obsunified/types";
|
|
18
|
+
import { getAllAnalysesForProject } from "../analyses/index";
|
|
19
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
20
|
+
import { AnalysesStore } from "../lib/analyses-store";
|
|
21
|
+
import { runAsk } from "../lib/ask";
|
|
22
|
+
import type { LlmConfig } from "../lib/llm";
|
|
23
|
+
import { sqlDbFor } from "../lib/sql-db";
|
|
24
|
+
import { getProjectId } from "./_context";
|
|
25
|
+
|
|
26
|
+
const MAX_QUESTION_CHARS = 1000;
|
|
27
|
+
|
|
28
|
+
export const askRoutesPlugin: CollectorPlugin = {
|
|
29
|
+
name: "ask-routes",
|
|
30
|
+
register(app, runtime) {
|
|
31
|
+
app.post("/internal/ask", async (c) => {
|
|
32
|
+
let body: Partial<AskRequest> = {};
|
|
33
|
+
try {
|
|
34
|
+
body = (await c.req.json()) as Partial<AskRequest>;
|
|
35
|
+
} catch {
|
|
36
|
+
return c.json(
|
|
37
|
+
{ error: "Bad Request", message: "body must be JSON" },
|
|
38
|
+
400,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const question = (body.question ?? "").trim();
|
|
42
|
+
if (!question) {
|
|
43
|
+
return c.json(
|
|
44
|
+
{ error: "Bad Request", message: "question is required" },
|
|
45
|
+
400,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (question.length > MAX_QUESTION_CHARS) {
|
|
49
|
+
return c.json(
|
|
50
|
+
{
|
|
51
|
+
error: "Bad Request",
|
|
52
|
+
message: `question exceeds ${MAX_QUESTION_CHARS} chars`,
|
|
53
|
+
},
|
|
54
|
+
400,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const projectId = getProjectId(c);
|
|
59
|
+
// OpenAI wins if both keys are set.
|
|
60
|
+
// Empty strings in .dev.vars come through as "" (not undefined),
|
|
61
|
+
// so coerce to undefined explicitly — otherwise an empty
|
|
62
|
+
// OPENAI_BASE_URL clobbers the default and we end up POSTing
|
|
63
|
+
// to a relative URL.
|
|
64
|
+
const openaiBase = c.env.OPENAI_BASE_URL?.trim()
|
|
65
|
+
? c.env.OPENAI_BASE_URL
|
|
66
|
+
: undefined;
|
|
67
|
+
const narrativeModel = c.env.NARRATIVE_MODEL?.trim()
|
|
68
|
+
? c.env.NARRATIVE_MODEL
|
|
69
|
+
: undefined;
|
|
70
|
+
const llm: LlmConfig | null = c.env.OPENAI_API_KEY?.trim()
|
|
71
|
+
? {
|
|
72
|
+
provider: "openai",
|
|
73
|
+
apiKey: c.env.OPENAI_API_KEY,
|
|
74
|
+
model: narrativeModel ?? "gpt-4o-mini",
|
|
75
|
+
apiUrl: openaiBase,
|
|
76
|
+
tracer: runtime.withChildSpan,
|
|
77
|
+
}
|
|
78
|
+
: c.env.ANTHROPIC_API_KEY?.trim()
|
|
79
|
+
? {
|
|
80
|
+
provider: "anthropic",
|
|
81
|
+
apiKey: c.env.ANTHROPIC_API_KEY,
|
|
82
|
+
model: narrativeModel ?? "claude-haiku-4-5",
|
|
83
|
+
tracer: runtime.withChildSpan,
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
86
|
+
if (!llm) {
|
|
87
|
+
const response: AskResponse = {
|
|
88
|
+
answer: null,
|
|
89
|
+
evidence: [],
|
|
90
|
+
queries: [],
|
|
91
|
+
error:
|
|
92
|
+
"Ask is not configured — set OPENAI_API_KEY or ANTHROPIC_API_KEY on the collector to enable it.",
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
return c.json(response, 503);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const store = new AnalysesStore(sqlDbFor(c.env));
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
runtime.logger.info("[ask] llm call starting", {
|
|
103
|
+
project_id: projectId,
|
|
104
|
+
provider: llm.provider,
|
|
105
|
+
model: llm.model,
|
|
106
|
+
question_chars: question.length,
|
|
107
|
+
});
|
|
108
|
+
const result = await runtime.withChildSpan(
|
|
109
|
+
"ask.runAsk",
|
|
110
|
+
() =>
|
|
111
|
+
runAsk(question, {
|
|
112
|
+
llm,
|
|
113
|
+
listAnalyses: async (filters) => {
|
|
114
|
+
// Pull from the live registry so the model sees Tier 1
|
|
115
|
+
// panels derived this tick, not stale ones in
|
|
116
|
+
// `analysis_definitions`.
|
|
117
|
+
const all = await getAllAnalysesForProject(projectId, {
|
|
118
|
+
db: sqlDbFor(c.env),
|
|
119
|
+
});
|
|
120
|
+
return all.filter((d) => {
|
|
121
|
+
if (filters?.group && d.group !== filters.group) return false;
|
|
122
|
+
if (filters?.view && d.view !== filters.view) return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
getLatestResult: async (id) => {
|
|
127
|
+
const definitions = await store.listDefinitions(projectId);
|
|
128
|
+
const definition = definitions.find((d) => d.id === id);
|
|
129
|
+
if (!definition) return null;
|
|
130
|
+
const r = await store.getLatestResult(projectId, id);
|
|
131
|
+
return { definition, result: r };
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
{
|
|
135
|
+
"llm.provider": llm.provider,
|
|
136
|
+
"llm.model": llm.model,
|
|
137
|
+
"openinference.span.kind": "LLM",
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
runtime.logger.info("[ask] llm call finished", {
|
|
141
|
+
project_id: projectId,
|
|
142
|
+
provider: llm.provider,
|
|
143
|
+
model: llm.model,
|
|
144
|
+
latency_ms: Date.now() - startedAt,
|
|
145
|
+
evidence_count: result.evidence.length,
|
|
146
|
+
query_count: result.queries.length,
|
|
147
|
+
answered: Boolean(result.answer),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Stage 6 — log evidence citations for the auto-pin derivation.
|
|
151
|
+
// Only on successful answers; failed loops would skew the signal
|
|
152
|
+
// (the model often calls every tool it has on the way to giving
|
|
153
|
+
// up). Best-effort: a write failure here doesn't fail the ask.
|
|
154
|
+
if (result.answer && result.evidence.length > 0) {
|
|
155
|
+
await store
|
|
156
|
+
.recordAskEvidence(
|
|
157
|
+
projectId,
|
|
158
|
+
result.evidence.map((e) => e.analysisId),
|
|
159
|
+
)
|
|
160
|
+
.catch((err) =>
|
|
161
|
+
runtime.logger.warn("[ask] recordAskEvidence failed", {
|
|
162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return c.json(result);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
runtime.logger.error("[ask] failed", { error: message });
|
|
171
|
+
const response: AskResponse = {
|
|
172
|
+
answer: null,
|
|
173
|
+
evidence: [],
|
|
174
|
+
queries: [],
|
|
175
|
+
error: message,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
};
|
|
178
|
+
return c.json(response, 500);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectorPlugin,
|
|
3
|
+
UsageEventProcessorPlugin,
|
|
4
|
+
} from "../framework/collector";
|
|
5
|
+
import { isBot } from "../lib/ua-parser";
|
|
6
|
+
|
|
7
|
+
const processor: UsageEventProcessorPlugin = {
|
|
8
|
+
name: "bot-filter",
|
|
9
|
+
process(events) {
|
|
10
|
+
return events.map((event) => ({ ...event, isBot: isBot(event.userAgent) }));
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const botFilterPlugin: CollectorPlugin = {
|
|
15
|
+
name: "bot-filter",
|
|
16
|
+
register(_app, runtime) {
|
|
17
|
+
runtime.addUsageEventProcessor(processor);
|
|
18
|
+
},
|
|
19
|
+
};
|